2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file industry_gui.cpp GUIs related to industries. */
13 #include "settings_gui.h"
14 #include "sound_func.h"
15 #include "window_func.h"
16 #include "textbuf_gui.h"
17 #include "command_func.h"
18 #include "viewport_func.h"
21 #include "cheat_type.h"
22 #include "newgrf_industries.h"
23 #include "newgrf_text.h"
24 #include "newgrf_debug.h"
25 #include "network/network.h"
26 #include "strings_func.h"
27 #include "company_func.h"
28 #include "tilehighlight_func.h"
29 #include "string_func.h"
30 #include "sortlist_type.h"
31 #include "dropdown_func.h"
32 #include "company_base.h"
33 #include "core/geometry_func.hpp"
34 #include "core/random_func.hpp"
35 #include "core/backup_type.hpp"
37 #include "smallmap_gui.h"
38 #include "dropdown_type.h"
39 #include "clear_map.h"
40 #include "zoom_func.h"
41 #include "industry_cmd.h"
42 #include "querystring_gui.h"
43 #include "stringfilter_type.h"
44 #include "timer/timer.h"
45 #include "timer/timer_window.h"
48 #include "widgets/industry_widget.h"
50 #include "table/strings.h"
54 #include "safeguards.h"
56 bool _ignore_restrictions
;
57 std::bitset
<NUM_INDUSTRYTYPES
> _displayed_industries
; ///< Communication from the industry chain window to the smallmap window about what industries to display.
59 /** Cargo suffix type (for which window is it requested) */
60 enum CargoSuffixType
{
61 CST_FUND
, ///< Fund-industry window
62 CST_VIEW
, ///< View-industry window
63 CST_DIR
, ///< Industry-directory window
66 /** Ways of displaying the cargo. */
67 enum CargoSuffixDisplay
{
68 CSD_CARGO
, ///< Display the cargo without sub-type (cb37 result 401).
69 CSD_CARGO_AMOUNT
, ///< Display the cargo and amount (if useful), but no sub-type (cb37 result 400 or fail).
70 CSD_CARGO_TEXT
, ///< Display then cargo and supplied string (cb37 result 800-BFF).
71 CSD_CARGO_AMOUNT_TEXT
, ///< Display then cargo, amount, and string (cb37 result 000-3FF).
74 /** Transfer storage of cargo suffix information. */
76 CargoSuffixDisplay display
; ///< How to display the cargo and text.
77 std::string text
; ///< Cargo suffix text.
80 extern void GenerateIndustries();
81 static void ShowIndustryCargoesWindow(IndustryType id
);
84 * Gets the string to display after the cargo name (using callback 37)
85 * @param cargo the cargo for which the suffix is requested, meaning depends on presence of flag 18 in prop 1A
86 * @param cst the cargo suffix type (for which window is it requested). @see CargoSuffixType
87 * @param ind the industry (nullptr if in fund window)
88 * @param ind_type the industry type
89 * @param indspec the industry spec
90 * @param suffix is filled with the string to display
92 static void GetCargoSuffix(uint cargo
, CargoSuffixType cst
, const Industry
*ind
, IndustryType ind_type
, const IndustrySpec
*indspec
, CargoSuffix
&suffix
)
95 suffix
.display
= CSD_CARGO_AMOUNT
;
97 if (HasBit(indspec
->callback_mask
, CBM_IND_CARGO_SUFFIX
)) {
98 TileIndex t
= (cst
!= CST_FUND
) ? ind
->location
.tile
: INVALID_TILE
;
99 uint16_t callback
= GetIndustryCallback(CBID_INDUSTRY_CARGO_SUFFIX
, 0, (cst
<< 8) | cargo
, const_cast<Industry
*>(ind
), ind_type
, t
);
100 if (callback
== CALLBACK_FAILED
) return;
102 if (indspec
->grf_prop
.grffile
->grf_version
< 8) {
103 if (GB(callback
, 0, 8) == 0xFF) return;
104 if (callback
< 0x400) {
105 StartTextRefStackUsage(indspec
->grf_prop
.grffile
, 6);
106 suffix
.text
= GetString(GetGRFStringID(indspec
->grf_prop
.grffile
->grfid
, 0xD000 + callback
));
107 StopTextRefStackUsage();
108 suffix
.display
= CSD_CARGO_AMOUNT_TEXT
;
111 ErrorUnknownCallbackResult(indspec
->grf_prop
.grffile
->grfid
, CBID_INDUSTRY_CARGO_SUFFIX
, callback
);
114 } else { // GRF version 8 or higher.
115 if (callback
== 0x400) return;
116 if (callback
== 0x401) {
117 suffix
.display
= CSD_CARGO
;
120 if (callback
< 0x400) {
121 StartTextRefStackUsage(indspec
->grf_prop
.grffile
, 6);
122 suffix
.text
= GetString(GetGRFStringID(indspec
->grf_prop
.grffile
->grfid
, 0xD000 + callback
));
123 StopTextRefStackUsage();
124 suffix
.display
= CSD_CARGO_AMOUNT_TEXT
;
127 if (callback
>= 0x800 && callback
< 0xC00) {
128 StartTextRefStackUsage(indspec
->grf_prop
.grffile
, 6);
129 suffix
.text
= GetString(GetGRFStringID(indspec
->grf_prop
.grffile
->grfid
, 0xD000 - 0x800 + callback
));
130 StopTextRefStackUsage();
131 suffix
.display
= CSD_CARGO_TEXT
;
134 ErrorUnknownCallbackResult(indspec
->grf_prop
.grffile
->grfid
, CBID_INDUSTRY_CARGO_SUFFIX
, callback
);
140 enum CargoSuffixInOut
{
146 * Gets all strings to display after the cargoes of industries (using callback 37)
147 * @param use_input get suffixes for output cargoes or input cargoes?
148 * @param cst the cargo suffix type (for which window is it requested). @see CargoSuffixType
149 * @param ind the industry (nullptr if in fund window)
150 * @param ind_type the industry type
151 * @param indspec the industry spec
152 * @param cargoes array with cargotypes. for INVALID_CARGO no suffix will be determined
153 * @param suffixes is filled with the suffixes
155 template <typename TC
, typename TS
>
156 static inline void GetAllCargoSuffixes(CargoSuffixInOut use_input
, CargoSuffixType cst
, const Industry
*ind
, IndustryType ind_type
, const IndustrySpec
*indspec
, const TC
&cargoes
, TS
&suffixes
)
158 static_assert(std::tuple_size_v
<std::remove_reference_t
<decltype(cargoes
)>> <= lengthof(suffixes
));
160 if (indspec
->behaviour
& INDUSTRYBEH_CARGOTYPES_UNLIMITED
) {
161 /* Reworked behaviour with new many-in-many-out scheme */
162 for (uint j
= 0; j
< lengthof(suffixes
); j
++) {
163 if (IsValidCargoID(cargoes
[j
])) {
164 uint8_t local_id
= indspec
->grf_prop
.grffile
->cargo_map
[cargoes
[j
]]; // should we check the value for valid?
165 uint cargotype
= local_id
<< 16 | use_input
;
166 GetCargoSuffix(cargotype
, cst
, ind
, ind_type
, indspec
, suffixes
[j
]);
168 suffixes
[j
].text
.clear();
169 suffixes
[j
].display
= CSD_CARGO
;
173 /* Compatible behaviour with old 3-in-2-out scheme */
174 for (uint j
= 0; j
< lengthof(suffixes
); j
++) {
175 suffixes
[j
].text
.clear();
176 suffixes
[j
].display
= CSD_CARGO
;
179 case CARGOSUFFIX_OUT
:
180 // Handle INDUSTRY_ORIGINAL_NUM_OUTPUTS cargoes
181 if (IsValidCargoID(cargoes
[0])) GetCargoSuffix(3, cst
, ind
, ind_type
, indspec
, suffixes
[0]);
182 if (IsValidCargoID(cargoes
[1])) GetCargoSuffix(4, cst
, ind
, ind_type
, indspec
, suffixes
[1]);
185 // Handle INDUSTRY_ORIGINAL_NUM_INPUTS cargoes
186 if (IsValidCargoID(cargoes
[0])) GetCargoSuffix(0, cst
, ind
, ind_type
, indspec
, suffixes
[0]);
187 if (IsValidCargoID(cargoes
[1])) GetCargoSuffix(1, cst
, ind
, ind_type
, indspec
, suffixes
[1]);
188 if (IsValidCargoID(cargoes
[2])) GetCargoSuffix(2, cst
, ind
, ind_type
, indspec
, suffixes
[2]);
197 * Gets the strings to display after the cargo of industries (using callback 37)
198 * @param use_input get suffixes for output cargo or input cargo?
199 * @param cst the cargo suffix type (for which window is it requested). @see CargoSuffixType
200 * @param ind the industry (nullptr if in fund window)
201 * @param ind_type the industry type
202 * @param indspec the industry spec
203 * @param cargo cargotype. for INVALID_CARGO no suffix will be determined
204 * @param slot accepts/produced slot number, used for old-style 3-in/2-out industries.
205 * @param suffix is filled with the suffix
207 void GetCargoSuffix(CargoSuffixInOut use_input
, CargoSuffixType cst
, const Industry
*ind
, IndustryType ind_type
, const IndustrySpec
*indspec
, CargoID cargo
, uint8_t slot
, CargoSuffix
&suffix
)
210 suffix
.display
= CSD_CARGO
;
211 if (!IsValidCargoID(cargo
)) return;
212 if (indspec
->behaviour
& INDUSTRYBEH_CARGOTYPES_UNLIMITED
) {
213 uint8_t local_id
= indspec
->grf_prop
.grffile
->cargo_map
[cargo
]; // should we check the value for valid?
214 uint cargotype
= local_id
<< 16 | use_input
;
215 GetCargoSuffix(cargotype
, cst
, ind
, ind_type
, indspec
, suffix
);
216 } else if (use_input
== CARGOSUFFIX_IN
) {
217 if (slot
< INDUSTRY_ORIGINAL_NUM_INPUTS
) GetCargoSuffix(slot
, cst
, ind
, ind_type
, indspec
, suffix
);
218 } else if (use_input
== CARGOSUFFIX_OUT
) {
219 if (slot
< INDUSTRY_ORIGINAL_NUM_OUTPUTS
) GetCargoSuffix(slot
+ INDUSTRY_ORIGINAL_NUM_INPUTS
, cst
, ind
, ind_type
, indspec
, suffix
);
223 std::array
<IndustryType
, NUM_INDUSTRYTYPES
> _sorted_industry_types
; ///< Industry types sorted by name.
225 /** Sort industry types by their name. */
226 static bool IndustryTypeNameSorter(const IndustryType
&a
, const IndustryType
&b
)
228 int r
= StrNaturalCompare(GetString(GetIndustrySpec(a
)->name
), GetString(GetIndustrySpec(b
)->name
)); // Sort by name (natural sorting).
230 /* If the names are equal, sort by industry type. */
231 return (r
!= 0) ? r
< 0 : (a
< b
);
235 * Initialize the list of sorted industry types.
237 void SortIndustryTypes()
239 /* Add each industry type to the list. */
240 for (IndustryType i
= 0; i
< NUM_INDUSTRYTYPES
; i
++) {
241 _sorted_industry_types
[i
] = i
;
244 /* Sort industry types by name. */
245 std::sort(_sorted_industry_types
.begin(), _sorted_industry_types
.end(), IndustryTypeNameSorter
);
249 * Command callback. In case of failure to build an industry, show an error message.
250 * @param result Result of the command.
251 * @param tile Tile where the industry is placed.
252 * @param indtype Industry type.
254 void CcBuildIndustry(Commands
, const CommandCost
&result
, TileIndex tile
, IndustryType indtype
, uint32_t, bool, uint32_t)
256 if (result
.Succeeded()) return;
258 if (indtype
< NUM_INDUSTRYTYPES
) {
259 const IndustrySpec
*indsp
= GetIndustrySpec(indtype
);
260 if (indsp
->enabled
) {
261 SetDParam(0, indsp
->name
);
262 ShowErrorMessage(STR_ERROR_CAN_T_BUILD_HERE
, result
.GetErrorMessage(), WL_INFO
, TileX(tile
) * TILE_SIZE
, TileY(tile
) * TILE_SIZE
);
267 static constexpr NWidgetPart _nested_build_industry_widgets
[] = {
268 NWidget(NWID_HORIZONTAL
),
269 NWidget(WWT_CLOSEBOX
, COLOUR_DARK_GREEN
),
270 NWidget(WWT_CAPTION
, COLOUR_DARK_GREEN
), SetDataTip(STR_FUND_INDUSTRY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
271 NWidget(WWT_SHADEBOX
, COLOUR_DARK_GREEN
),
272 NWidget(WWT_DEFSIZEBOX
, COLOUR_DARK_GREEN
),
273 NWidget(WWT_STICKYBOX
, COLOUR_DARK_GREEN
),
275 NWidget(NWID_SELECTION
, COLOUR_DARK_GREEN
, WID_DPI_SCENARIO_EDITOR_PANE
),
276 NWidget(NWID_VERTICAL
),
277 NWidget(WWT_TEXTBTN
, COLOUR_DARK_GREEN
, WID_DPI_CREATE_RANDOM_INDUSTRIES_WIDGET
), SetMinimalSize(0, 12), SetFill(1, 0), SetResize(1, 0),
278 SetDataTip(STR_FUND_INDUSTRY_MANY_RANDOM_INDUSTRIES
, STR_FUND_INDUSTRY_MANY_RANDOM_INDUSTRIES_TOOLTIP
),
279 NWidget(WWT_TEXTBTN
, COLOUR_DARK_GREEN
, WID_DPI_REMOVE_ALL_INDUSTRIES_WIDGET
), SetMinimalSize(0, 12), SetFill(1, 0), SetResize(1, 0),
280 SetDataTip(STR_FUND_INDUSTRY_REMOVE_ALL_INDUSTRIES
, STR_FUND_INDUSTRY_REMOVE_ALL_INDUSTRIES_TOOLTIP
),
283 NWidget(NWID_HORIZONTAL
),
284 NWidget(WWT_MATRIX
, COLOUR_DARK_GREEN
, WID_DPI_MATRIX_WIDGET
), SetMatrixDataTip(1, 0, STR_FUND_INDUSTRY_SELECTION_TOOLTIP
), SetFill(1, 0), SetResize(1, 1), SetScrollbar(WID_DPI_SCROLLBAR
),
285 NWidget(NWID_VSCROLLBAR
, COLOUR_DARK_GREEN
, WID_DPI_SCROLLBAR
),
287 NWidget(WWT_PANEL
, COLOUR_DARK_GREEN
, WID_DPI_INFOPANEL
), SetResize(1, 0),
289 NWidget(NWID_HORIZONTAL
),
290 NWidget(WWT_TEXTBTN
, COLOUR_DARK_GREEN
, WID_DPI_DISPLAY_WIDGET
), SetFill(1, 0), SetResize(1, 0),
291 SetDataTip(STR_INDUSTRY_DISPLAY_CHAIN
, STR_INDUSTRY_DISPLAY_CHAIN_TOOLTIP
),
292 NWidget(WWT_TEXTBTN
, COLOUR_DARK_GREEN
, WID_DPI_FUND_WIDGET
), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_JUST_STRING
, STR_NULL
),
293 NWidget(WWT_RESIZEBOX
, COLOUR_DARK_GREEN
),
297 /** Window definition of the dynamic place industries gui */
298 static WindowDesc
_build_industry_desc(
299 WDP_AUTO
, "build_industry", 170, 212,
300 WC_BUILD_INDUSTRY
, WC_NONE
,
302 _nested_build_industry_widgets
305 /** Build (fund or prospect) a new industry, */
306 class BuildIndustryWindow
: public Window
{
307 IndustryType selected_type
; ///< industry corresponding to the above index
308 std::vector
<IndustryType
> list
; ///< List of industries.
309 bool enabled
; ///< Availability state of the selected industry.
311 Dimension legend
; ///< Dimension of the legend 'blob'.
313 /** The largest allowed minimum-width of the window, given in line heights */
314 static const int MAX_MINWIDTH_LINEHEIGHTS
= 20;
316 void UpdateAvailability()
318 this->enabled
= this->selected_type
!= INVALID_INDUSTRYTYPE
&& (_game_mode
== GM_EDITOR
|| GetIndustryProbabilityCallback(this->selected_type
, IACT_USERCREATION
, 1) > 0);
325 /* Fill the arrays with industries.
326 * The tests performed after the enabled allow to load the industries
327 * In the same way they are inserted by grf (if any)
329 for (IndustryType ind
: _sorted_industry_types
) {
330 const IndustrySpec
*indsp
= GetIndustrySpec(ind
);
331 if (indsp
->enabled
) {
332 /* Rule is that editor mode loads all industries.
333 * In game mode, all non raw industries are loaded too
334 * and raw ones are loaded only when setting allows it */
335 if (_game_mode
!= GM_EDITOR
&& indsp
->IsRawIndustry() && _settings_game
.construction
.raw_industry_construction
== 0) {
336 /* Unselect if the industry is no longer in the list */
337 if (this->selected_type
== ind
) this->selected_type
= INVALID_INDUSTRYTYPE
;
341 this->list
.push_back(ind
);
345 /* First industry type is selected if the current selection is invalid. */
346 if (this->selected_type
== INVALID_INDUSTRYTYPE
&& !this->list
.empty()) this->selected_type
= this->list
[0];
348 this->UpdateAvailability();
350 this->vscroll
->SetCount(this->list
.size());
353 /** Update status of the fund and display-chain widgets. */
356 this->SetWidgetDisabledState(WID_DPI_FUND_WIDGET
, this->selected_type
!= INVALID_INDUSTRYTYPE
&& !this->enabled
);
357 this->SetWidgetDisabledState(WID_DPI_DISPLAY_WIDGET
, this->selected_type
== INVALID_INDUSTRYTYPE
&& this->enabled
);
361 * Build a string of cargo names with suffixes attached.
362 * This is distinct from the CARGO_LIST string formatting code in two ways:
363 * - This cargo list uses the order defined by the industry, rather than alphabetic.
364 * - NewGRF-supplied suffix strings can be attached to each cargo.
366 * @param cargolist Array of CargoID to display
367 * @param cargo_suffix Array of suffixes to attach to each cargo
368 * @param cargolistlen Length of arrays
369 * @param prefixstr String to use for the first item
370 * @return A formatted raw string
372 std::string
MakeCargoListString(const std::span
<const CargoID
> cargolist
, const std::span
<const CargoSuffix
> cargo_suffix
, StringID prefixstr
) const
374 assert(cargolist
.size() == cargo_suffix
.size());
376 std::string cargostring
;
378 size_t firstcargo
= 0;
380 for (size_t j
= 0; j
< cargolist
.size(); j
++) {
381 if (!IsValidCargoID(cargolist
[j
])) continue;
387 SetDParam(0, CargoSpec::Get(cargolist
[j
])->name
);
388 SetDParamStr(1, cargo_suffix
[j
].text
);
389 cargostring
+= GetString(STR_INDUSTRY_VIEW_CARGO_LIST_EXTENSION
);
393 SetDParam(0, CargoSpec::Get(cargolist
[firstcargo
])->name
);
394 SetDParamStr(1, cargo_suffix
[firstcargo
].text
);
395 cargostring
= GetString(prefixstr
) + cargostring
;
397 SetDParam(0, STR_JUST_NOTHING
);
399 cargostring
= GetString(prefixstr
);
406 BuildIndustryWindow() : Window(_build_industry_desc
)
408 this->selected_type
= INVALID_INDUSTRYTYPE
;
410 this->CreateNestedTree();
411 this->vscroll
= this->GetScrollbar(WID_DPI_SCROLLBAR
);
412 /* Show scenario editor tools in editor. */
413 if (_game_mode
!= GM_EDITOR
) {
414 this->GetWidget
<NWidgetStacked
>(WID_DPI_SCENARIO_EDITOR_PANE
)->SetDisplayedPlane(SZSP_HORIZONTAL
);
416 this->FinishInitNested(0);
421 void OnInit() override
423 /* Width of the legend blob -- slightly larger than the smallmap legend blob. */
424 this->legend
.height
= GetCharacterHeight(FS_SMALL
);
425 this->legend
.width
= this->legend
.height
* 9 / 6;
430 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
433 case WID_DPI_MATRIX_WIDGET
: {
434 SetDParamMaxDigits(0, 4);
435 Dimension count
= GetStringBoundingBox(STR_JUST_COMMA
, FS_SMALL
);
437 for (const auto &indtype
: this->list
) {
438 d
= maxdim(d
, GetStringBoundingBox(GetIndustrySpec(indtype
)->name
));
440 resize
.height
= std::max
<uint
>({this->legend
.height
, d
.height
, count
.height
}) + padding
.height
;
441 d
.width
+= this->legend
.width
+ WidgetDimensions::scaled
.hsep_wide
+ WidgetDimensions::scaled
.hsep_normal
+ count
.width
+ padding
.width
;
442 d
.height
= 5 * resize
.height
;
443 size
= maxdim(size
, d
);
447 case WID_DPI_INFOPANEL
: {
448 /* Extra line for cost outside of editor. */
449 int height
= 2 + (_game_mode
== GM_EDITOR
? 0 : 1);
450 uint extra_lines_req
= 0;
451 uint extra_lines_prd
= 0;
452 uint extra_lines_newgrf
= 0;
453 uint max_minwidth
= GetCharacterHeight(FS_NORMAL
) * MAX_MINWIDTH_LINEHEIGHTS
;
454 Dimension d
= {0, 0};
455 for (const auto &indtype
: this->list
) {
456 const IndustrySpec
*indsp
= GetIndustrySpec(indtype
);
457 CargoSuffix cargo_suffix
[std::tuple_size_v
<decltype(indsp
->accepts_cargo
)>];
459 /* Measure the accepted cargoes, if any. */
460 GetAllCargoSuffixes(CARGOSUFFIX_IN
, CST_FUND
, nullptr, indtype
, indsp
, indsp
->accepts_cargo
, cargo_suffix
);
461 std::string cargostring
= this->MakeCargoListString(indsp
->accepts_cargo
, cargo_suffix
, STR_INDUSTRY_VIEW_REQUIRES_N_CARGO
);
462 Dimension strdim
= GetStringBoundingBox(cargostring
);
463 if (strdim
.width
> max_minwidth
) {
464 extra_lines_req
= std::max(extra_lines_req
, strdim
.width
/ max_minwidth
+ 1);
465 strdim
.width
= max_minwidth
;
467 d
= maxdim(d
, strdim
);
469 /* Measure the produced cargoes, if any. */
470 GetAllCargoSuffixes(CARGOSUFFIX_OUT
, CST_FUND
, nullptr, indtype
, indsp
, indsp
->produced_cargo
, cargo_suffix
);
471 cargostring
= this->MakeCargoListString(indsp
->produced_cargo
, cargo_suffix
, STR_INDUSTRY_VIEW_PRODUCES_N_CARGO
);
472 strdim
= GetStringBoundingBox(cargostring
);
473 if (strdim
.width
> max_minwidth
) {
474 extra_lines_prd
= std::max(extra_lines_prd
, strdim
.width
/ max_minwidth
+ 1);
475 strdim
.width
= max_minwidth
;
477 d
= maxdim(d
, strdim
);
479 if (indsp
->grf_prop
.grffile
!= nullptr) {
480 /* Reserve a few extra lines for text from an industry NewGRF. */
481 extra_lines_newgrf
= 4;
485 /* Set it to something more sane :) */
486 height
+= extra_lines_prd
+ extra_lines_req
+ extra_lines_newgrf
;
487 size
.height
= height
* GetCharacterHeight(FS_NORMAL
) + padding
.height
;
488 size
.width
= d
.width
+ padding
.width
;
492 case WID_DPI_FUND_WIDGET
: {
493 Dimension d
= GetStringBoundingBox(STR_FUND_INDUSTRY_BUILD_NEW_INDUSTRY
);
494 d
= maxdim(d
, GetStringBoundingBox(STR_FUND_INDUSTRY_PROSPECT_NEW_INDUSTRY
));
495 d
= maxdim(d
, GetStringBoundingBox(STR_FUND_INDUSTRY_FUND_NEW_INDUSTRY
));
496 d
.width
+= padding
.width
;
497 d
.height
+= padding
.height
;
498 size
= maxdim(size
, d
);
504 void SetStringParameters(WidgetID widget
) const override
507 case WID_DPI_FUND_WIDGET
:
508 /* Raw industries might be prospected. Show this fact by changing the string
509 * In Editor, you just build, while ingame, or you fund or you prospect */
510 if (_game_mode
== GM_EDITOR
) {
511 /* We've chosen many random industries but no industries have been specified */
512 SetDParam(0, STR_FUND_INDUSTRY_BUILD_NEW_INDUSTRY
);
514 if (this->selected_type
!= INVALID_INDUSTRYTYPE
) {
515 const IndustrySpec
*indsp
= GetIndustrySpec(this->selected_type
);
516 SetDParam(0, (_settings_game
.construction
.raw_industry_construction
== 2 && indsp
->IsRawIndustry()) ? STR_FUND_INDUSTRY_PROSPECT_NEW_INDUSTRY
: STR_FUND_INDUSTRY_FUND_NEW_INDUSTRY
);
518 SetDParam(0, STR_FUND_INDUSTRY_FUND_NEW_INDUSTRY
);
525 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
528 case WID_DPI_MATRIX_WIDGET
: {
529 bool rtl
= _current_text_dir
== TD_RTL
;
530 Rect text
= r
.WithHeight(this->resize
.step_height
).Shrink(WidgetDimensions::scaled
.matrix
);
531 Rect icon
= text
.WithWidth(this->legend
.width
, rtl
);
532 text
= text
.Indent(this->legend
.width
+ WidgetDimensions::scaled
.hsep_wide
, rtl
);
534 /* Vertical offset for legend icon. */
535 icon
.top
= r
.top
+ (this->resize
.step_height
- this->legend
.height
+ 1) / 2;
536 icon
.bottom
= icon
.top
+ this->legend
.height
- 1;
538 auto [first
, last
] = this->vscroll
->GetVisibleRangeIterators(this->list
);
539 for (auto it
= first
; it
!= last
; ++it
) {
540 IndustryType type
= *it
;
541 bool selected
= this->selected_type
== type
;
542 const IndustrySpec
*indsp
= GetIndustrySpec(type
);
544 /* Draw the name of the industry in white is selected, otherwise, in orange */
545 DrawString(text
, indsp
->name
, selected
? TC_WHITE
: TC_ORANGE
);
546 GfxFillRect(icon
, selected
? PC_WHITE
: PC_BLACK
);
547 GfxFillRect(icon
.Shrink(WidgetDimensions::scaled
.bevel
), indsp
->map_colour
);
548 SetDParam(0, Industry::GetIndustryTypeCount(type
));
549 DrawString(text
, STR_JUST_COMMA
, TC_BLACK
, SA_RIGHT
, false, FS_SMALL
);
551 text
= text
.Translate(0, this->resize
.step_height
);
552 icon
= icon
.Translate(0, this->resize
.step_height
);
557 case WID_DPI_INFOPANEL
: {
558 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
560 if (this->selected_type
== INVALID_INDUSTRYTYPE
) {
561 DrawStringMultiLine(ir
, STR_FUND_INDUSTRY_MANY_RANDOM_INDUSTRIES_TOOLTIP
);
565 const IndustrySpec
*indsp
= GetIndustrySpec(this->selected_type
);
567 if (_game_mode
!= GM_EDITOR
) {
568 SetDParam(0, indsp
->GetConstructionCost());
569 DrawString(ir
, STR_FUND_INDUSTRY_INDUSTRY_BUILD_COST
);
570 ir
.top
+= GetCharacterHeight(FS_NORMAL
);
573 CargoSuffix cargo_suffix
[std::tuple_size_v
<decltype(indsp
->accepts_cargo
)>];
575 /* Draw the accepted cargoes, if any. Otherwise, will print "Nothing". */
576 GetAllCargoSuffixes(CARGOSUFFIX_IN
, CST_FUND
, nullptr, this->selected_type
, indsp
, indsp
->accepts_cargo
, cargo_suffix
);
577 std::string cargostring
= this->MakeCargoListString(indsp
->accepts_cargo
, cargo_suffix
, STR_INDUSTRY_VIEW_REQUIRES_N_CARGO
);
578 ir
.top
= DrawStringMultiLine(ir
, cargostring
);
580 /* Draw the produced cargoes, if any. Otherwise, will print "Nothing". */
581 GetAllCargoSuffixes(CARGOSUFFIX_OUT
, CST_FUND
, nullptr, this->selected_type
, indsp
, indsp
->produced_cargo
, cargo_suffix
);
582 cargostring
= this->MakeCargoListString(indsp
->produced_cargo
, cargo_suffix
, STR_INDUSTRY_VIEW_PRODUCES_N_CARGO
);
583 ir
.top
= DrawStringMultiLine(ir
, cargostring
);
585 /* Get the additional purchase info text, if it has not already been queried. */
586 if (HasBit(indsp
->callback_mask
, CBM_IND_FUND_MORE_TEXT
)) {
587 uint16_t callback_res
= GetIndustryCallback(CBID_INDUSTRY_FUND_MORE_TEXT
, 0, 0, nullptr, this->selected_type
, INVALID_TILE
);
588 if (callback_res
!= CALLBACK_FAILED
&& callback_res
!= 0x400) {
589 if (callback_res
> 0x400) {
590 ErrorUnknownCallbackResult(indsp
->grf_prop
.grffile
->grfid
, CBID_INDUSTRY_FUND_MORE_TEXT
, callback_res
);
592 StringID str
= GetGRFStringID(indsp
->grf_prop
.grffile
->grfid
, 0xD000 + callback_res
); // No. here's the new string
593 if (str
!= STR_UNDEFINED
) {
594 StartTextRefStackUsage(indsp
->grf_prop
.grffile
, 6);
595 DrawStringMultiLine(ir
, str
, TC_YELLOW
);
596 StopTextRefStackUsage();
606 static void AskManyRandomIndustriesCallback(Window
*, bool confirmed
)
608 if (!confirmed
) return;
610 if (Town::GetNumItems() == 0) {
611 ShowErrorMessage(STR_ERROR_CAN_T_GENERATE_INDUSTRIES
, STR_ERROR_MUST_FOUND_TOWN_FIRST
, WL_INFO
);
613 Backup
<bool> old_generating_world(_generating_world
, true);
614 BasePersistentStorageArray::SwitchMode(PSM_ENTER_GAMELOOP
);
615 GenerateIndustries();
616 BasePersistentStorageArray::SwitchMode(PSM_LEAVE_GAMELOOP
);
617 old_generating_world
.Restore();
621 static void AskRemoveAllIndustriesCallback(Window
*, bool confirmed
)
623 if (!confirmed
) return;
625 for (Industry
*industry
: Industry::Iterate()) delete industry
;
627 /* Clear farmland. */
628 for (TileIndex tile
= 0; tile
< Map::Size(); tile
++) {
629 if (IsTileType(tile
, MP_CLEAR
) && GetRawClearGround(tile
) == CLEAR_FIELDS
) {
630 MakeClear(tile
, CLEAR_GRASS
, 3);
634 MarkWholeScreenDirty();
637 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
640 case WID_DPI_CREATE_RANDOM_INDUSTRIES_WIDGET
: {
641 assert(_game_mode
== GM_EDITOR
);
642 this->HandleButtonClick(WID_DPI_CREATE_RANDOM_INDUSTRIES_WIDGET
);
643 ShowQuery(STR_FUND_INDUSTRY_MANY_RANDOM_INDUSTRIES_CAPTION
, STR_FUND_INDUSTRY_MANY_RANDOM_INDUSTRIES_QUERY
, nullptr, AskManyRandomIndustriesCallback
);
647 case WID_DPI_REMOVE_ALL_INDUSTRIES_WIDGET
: {
648 assert(_game_mode
== GM_EDITOR
);
649 this->HandleButtonClick(WID_DPI_REMOVE_ALL_INDUSTRIES_WIDGET
);
650 ShowQuery(STR_FUND_INDUSTRY_REMOVE_ALL_INDUSTRIES_CAPTION
, STR_FUND_INDUSTRY_REMOVE_ALL_INDUSTRIES_QUERY
, nullptr, AskRemoveAllIndustriesCallback
);
654 case WID_DPI_MATRIX_WIDGET
: {
655 auto it
= this->vscroll
->GetScrolledItemFromWidget(this->list
, pt
.y
, this, WID_DPI_MATRIX_WIDGET
);
656 if (it
!= this->list
.end()) { // Is it within the boundaries of available data?
657 this->selected_type
= *it
;
658 this->UpdateAvailability();
660 const IndustrySpec
*indsp
= GetIndustrySpec(this->selected_type
);
664 if (_thd
.GetCallbackWnd() == this &&
665 ((_game_mode
!= GM_EDITOR
&& _settings_game
.construction
.raw_industry_construction
== 2 && indsp
!= nullptr && indsp
->IsRawIndustry()) || !this->enabled
)) {
666 /* Reset the button state if going to prospecting or "build many industries" */
667 this->RaiseButtons();
668 ResetObjectToPlace();
672 if (this->enabled
&& click_count
> 1) this->OnClick(pt
, WID_DPI_FUND_WIDGET
, 1);
677 case WID_DPI_DISPLAY_WIDGET
:
678 if (this->selected_type
!= INVALID_INDUSTRYTYPE
) ShowIndustryCargoesWindow(this->selected_type
);
681 case WID_DPI_FUND_WIDGET
: {
682 if (this->selected_type
!= INVALID_INDUSTRYTYPE
) {
683 if (_game_mode
!= GM_EDITOR
&& _settings_game
.construction
.raw_industry_construction
== 2 && GetIndustrySpec(this->selected_type
)->IsRawIndustry()) {
684 Command
<CMD_BUILD_INDUSTRY
>::Post(STR_ERROR_CAN_T_CONSTRUCT_THIS_INDUSTRY
, 0, this->selected_type
, 0, false, InteractiveRandom());
685 this->HandleButtonClick(WID_DPI_FUND_WIDGET
);
687 HandlePlacePushButton(this, WID_DPI_FUND_WIDGET
, SPR_CURSOR_INDUSTRY
, HT_RECT
);
695 void OnResize() override
697 /* Adjust the number of items in the matrix depending of the resize */
698 this->vscroll
->SetCapacityFromWidget(this, WID_DPI_MATRIX_WIDGET
);
701 void OnPlaceObject([[maybe_unused
]] Point pt
, TileIndex tile
) override
704 /* We do not need to protect ourselves against "Random Many Industries" in this mode */
705 const IndustrySpec
*indsp
= GetIndustrySpec(this->selected_type
);
706 uint32_t seed
= InteractiveRandom();
707 uint32_t layout_index
= InteractiveRandomRange((uint32_t)indsp
->layouts
.size());
709 if (_game_mode
== GM_EDITOR
) {
710 /* Show error if no town exists at all */
711 if (Town::GetNumItems() == 0) {
712 SetDParam(0, indsp
->name
);
713 ShowErrorMessage(STR_ERROR_CAN_T_BUILD_HERE
, STR_ERROR_MUST_FOUND_TOWN_FIRST
, WL_INFO
, pt
.x
, pt
.y
);
717 Backup
<CompanyID
> cur_company(_current_company
, OWNER_NONE
);
718 Backup
<bool> old_generating_world(_generating_world
, true);
719 _ignore_restrictions
= true;
721 Command
<CMD_BUILD_INDUSTRY
>::Post(STR_ERROR_CAN_T_CONSTRUCT_THIS_INDUSTRY
, &CcBuildIndustry
, tile
, this->selected_type
, layout_index
, false, seed
);
723 cur_company
.Restore();
724 old_generating_world
.Restore();
725 _ignore_restrictions
= false;
727 success
= Command
<CMD_BUILD_INDUSTRY
>::Post(STR_ERROR_CAN_T_CONSTRUCT_THIS_INDUSTRY
, tile
, this->selected_type
, layout_index
, false, seed
);
730 /* If an industry has been built, just reset the cursor and the system */
731 if (success
&& !_settings_client
.gui
.persistent_buildingtools
) ResetObjectToPlace();
734 IntervalTimer
<TimerWindow
> update_interval
= {std::chrono::seconds(3), [this](auto) {
735 if (_game_mode
== GM_EDITOR
) return;
736 if (this->selected_type
== INVALID_INDUSTRYTYPE
) return;
738 bool enabled
= this->enabled
;
739 this->UpdateAvailability();
740 if (enabled
!= this->enabled
) {
746 void OnTimeout() override
748 this->RaiseButtons();
751 void OnPlaceObjectAbort() override
753 this->RaiseButtons();
757 * Some data on this window has become invalid.
758 * @param data Information about the changed data.
759 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
761 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
763 if (!gui_scope
) return;
770 void ShowBuildIndustryWindow()
772 if (_game_mode
!= GM_EDITOR
&& !Company::IsValidID(_local_company
)) return;
773 if (BringWindowToFrontById(WC_BUILD_INDUSTRY
, 0)) return;
774 new BuildIndustryWindow();
777 static void UpdateIndustryProduction(Industry
*i
);
779 static inline bool IsProductionAlterable(const Industry
*i
)
781 const IndustrySpec
*is
= GetIndustrySpec(i
->type
);
782 bool has_prod
= std::any_of(std::begin(is
->production_rate
), std::end(is
->production_rate
), [](auto rate
) { return rate
!= 0; });
783 return ((_game_mode
== GM_EDITOR
|| _cheats
.setup_prod
.value
) &&
784 (has_prod
|| is
->IsRawIndustry()) &&
788 class IndustryViewWindow
: public Window
790 /** Modes for changing production */
792 EA_NONE
, ///< Not alterable
793 EA_MULTIPLIER
, ///< Allow changing the production multiplier
794 EA_RATE
, ///< Allow changing the production rates
797 /** Specific lines in the info panel */
799 IL_NONE
, ///< No line
800 IL_MULTIPLIER
, ///< Production multiplier
801 IL_RATE1
, ///< Production rate of cargo 1
802 IL_RATE2
, ///< Production rate of cargo 2
805 Dimension cargo_icon_size
; ///< Largest cargo icon dimension.
806 Editability editable
; ///< Mode for changing production
807 InfoLine editbox_line
; ///< The line clicked to open the edit box
808 InfoLine clicked_line
; ///< The line of the button that has been clicked
809 uint8_t clicked_button
; ///< The button that has been clicked (to raise)
810 int production_offset_y
; ///< The offset of the production texts/buttons
811 int info_height
; ///< Height needed for the #WID_IV_INFO panel
812 int cheat_line_height
; ///< Height of each line for the #WID_IV_INFO panel
815 IndustryViewWindow(WindowDesc
&desc
, WindowNumber window_number
) : Window(desc
)
817 this->flags
|= WF_DISABLE_VP_SCROLL
;
818 this->editbox_line
= IL_NONE
;
819 this->clicked_line
= IL_NONE
;
820 this->clicked_button
= 0;
821 this->info_height
= WidgetDimensions::scaled
.framerect
.Vertical() + 2 * GetCharacterHeight(FS_NORMAL
); // Info panel has at least two lines text.
823 this->InitNested(window_number
);
824 NWidgetViewport
*nvp
= this->GetWidget
<NWidgetViewport
>(WID_IV_VIEWPORT
);
825 nvp
->InitializeViewport(this, Industry::Get(window_number
)->location
.GetCenterTile(), ScaleZoomGUI(ZOOM_LVL_INDUSTRY
));
827 this->InvalidateData();
830 void OnInit() override
832 /* This only used when the cheat to alter industry production is enabled */
833 this->cheat_line_height
= std::max(SETTING_BUTTON_HEIGHT
+ WidgetDimensions::scaled
.vsep_normal
, GetCharacterHeight(FS_NORMAL
));
834 this->cargo_icon_size
= GetLargestCargoIconSize();
837 void OnPaint() override
841 if (this->IsShaded()) return; // Don't draw anything when the window is shaded.
843 const Rect r
= this->GetWidget
<NWidgetBase
>(WID_IV_INFO
)->GetCurrentRect();
844 int expected
= this->DrawInfo(r
);
845 if (expected
!= r
.bottom
) {
846 this->info_height
= expected
- r
.top
+ 1;
852 void DrawCargoIcon(const Rect
&r
, CargoID cid
) const
854 bool rtl
= _current_text_dir
== TD_RTL
;
855 SpriteID icon
= CargoSpec::Get(cid
)->GetCargoIcon();
856 Dimension d
= GetSpriteSize(icon
);
857 Rect ir
= r
.WithWidth(this->cargo_icon_size
.width
, rtl
).WithHeight(GetCharacterHeight(FS_NORMAL
));
858 DrawSprite(icon
, PAL_NONE
, CenterBounds(ir
.left
, ir
.right
, d
.width
), CenterBounds(ir
.top
, ir
.bottom
, this->cargo_icon_size
.height
));
862 * Draw the text in the #WID_IV_INFO panel.
863 * @param r Rectangle of the panel.
864 * @return Expected position of the bottom edge of the panel.
866 int DrawInfo(const Rect
&r
)
868 bool rtl
= _current_text_dir
== TD_RTL
;
869 Industry
*i
= Industry::Get(this->window_number
);
870 const IndustrySpec
*ind
= GetIndustrySpec(i
->type
);
871 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
873 bool has_accept
= false;
875 if (i
->prod_level
== PRODLEVEL_CLOSURE
) {
876 DrawString(ir
, STR_INDUSTRY_VIEW_INDUSTRY_ANNOUNCED_CLOSURE
);
877 ir
.top
+= GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.vsep_wide
;
880 const int label_indent
= WidgetDimensions::scaled
.hsep_normal
+ this->cargo_icon_size
.width
;
881 bool stockpiling
= HasBit(ind
->callback_mask
, CBM_IND_PRODUCTION_CARGO_ARRIVAL
) || HasBit(ind
->callback_mask
, CBM_IND_PRODUCTION_256_TICKS
);
883 for (const auto &a
: i
->accepted
) {
884 if (!IsValidCargoID(a
.cargo
)) continue;
887 DrawString(ir
, STR_INDUSTRY_VIEW_REQUIRES
);
888 ir
.top
+= GetCharacterHeight(FS_NORMAL
);
892 DrawCargoIcon(ir
, a
.cargo
);
895 GetCargoSuffix(CARGOSUFFIX_IN
, CST_VIEW
, i
, i
->type
, ind
, a
.cargo
, &a
- i
->accepted
.data(), suffix
);
897 SetDParam(0, CargoSpec::Get(a
.cargo
)->name
);
898 SetDParam(1, a
.cargo
);
899 SetDParam(2, a
.waiting
);
901 StringID str
= STR_NULL
;
902 switch (suffix
.display
) {
903 case CSD_CARGO_AMOUNT_TEXT
:
904 SetDParamStr(3, suffix
.text
);
906 case CSD_CARGO_AMOUNT
:
907 str
= stockpiling
? STR_INDUSTRY_VIEW_ACCEPT_CARGO_AMOUNT
: STR_INDUSTRY_VIEW_ACCEPT_CARGO
;
911 SetDParamStr(3, suffix
.text
);
914 str
= STR_INDUSTRY_VIEW_ACCEPT_CARGO
;
920 DrawString(ir
.Indent(label_indent
, rtl
), str
);
921 ir
.top
+= GetCharacterHeight(FS_NORMAL
);
924 int line_height
= this->editable
== EA_RATE
? this->cheat_line_height
: GetCharacterHeight(FS_NORMAL
);
925 int text_y_offset
= (line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
926 int button_y_offset
= (line_height
- SETTING_BUTTON_HEIGHT
) / 2;
928 for (const auto &p
: i
->produced
) {
929 if (!IsValidCargoID(p
.cargo
)) continue;
931 if (has_accept
) ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
932 DrawString(ir
, TimerGameEconomy::UsingWallclockUnits() ? STR_INDUSTRY_VIEW_PRODUCTION_LAST_MINUTE_TITLE
: STR_INDUSTRY_VIEW_PRODUCTION_LAST_MONTH_TITLE
);
933 ir
.top
+= GetCharacterHeight(FS_NORMAL
);
934 if (this->editable
== EA_RATE
) this->production_offset_y
= ir
.top
;
938 DrawCargoIcon(ir
, p
.cargo
);
941 GetCargoSuffix(CARGOSUFFIX_OUT
, CST_VIEW
, i
, i
->type
, ind
, p
.cargo
, &p
- i
->produced
.data(), suffix
);
943 SetDParam(0, p
.cargo
);
944 SetDParam(1, p
.history
[LAST_MONTH
].production
);
945 SetDParamStr(2, suffix
.text
);
946 SetDParam(3, ToPercent8(p
.history
[LAST_MONTH
].PctTransported()));
947 DrawString(ir
.Indent(label_indent
+ (this->editable
== EA_RATE
? SETTING_BUTTON_WIDTH
+ WidgetDimensions::scaled
.hsep_normal
: 0), rtl
).Translate(0, text_y_offset
), STR_INDUSTRY_VIEW_TRANSPORTED
);
948 /* Let's put out those buttons.. */
949 if (this->editable
== EA_RATE
) {
950 DrawArrowButtons(ir
.Indent(label_indent
, rtl
).WithWidth(SETTING_BUTTON_WIDTH
, rtl
).left
, ir
.top
+ button_y_offset
, COLOUR_YELLOW
, (this->clicked_line
== IL_RATE1
+ (&p
- i
->produced
.data())) ? this->clicked_button
: 0,
951 p
.rate
> 0, p
.rate
< 255);
953 ir
.top
+= line_height
;
956 /* Display production multiplier if editable */
957 if (this->editable
== EA_MULTIPLIER
) {
958 line_height
= this->cheat_line_height
;
959 text_y_offset
= (line_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
960 button_y_offset
= (line_height
- SETTING_BUTTON_HEIGHT
) / 2;
961 ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
962 this->production_offset_y
= ir
.top
;
963 SetDParam(0, RoundDivSU(i
->prod_level
* 100, PRODLEVEL_DEFAULT
));
964 DrawString(ir
.Indent(label_indent
+ SETTING_BUTTON_WIDTH
+ WidgetDimensions::scaled
.hsep_normal
, rtl
).Translate(0, text_y_offset
), STR_INDUSTRY_VIEW_PRODUCTION_LEVEL
);
965 DrawArrowButtons(ir
.Indent(label_indent
, rtl
).WithWidth(SETTING_BUTTON_WIDTH
, rtl
).left
, ir
.top
+ button_y_offset
, COLOUR_YELLOW
, (this->clicked_line
== IL_MULTIPLIER
) ? this->clicked_button
: 0,
966 i
->prod_level
> PRODLEVEL_MINIMUM
, i
->prod_level
< PRODLEVEL_MAXIMUM
);
967 ir
.top
+= line_height
;
970 /* Get the extra message for the GUI */
971 if (HasBit(ind
->callback_mask
, CBM_IND_WINDOW_MORE_TEXT
)) {
972 uint16_t callback_res
= GetIndustryCallback(CBID_INDUSTRY_WINDOW_MORE_TEXT
, 0, 0, i
, i
->type
, i
->location
.tile
);
973 if (callback_res
!= CALLBACK_FAILED
&& callback_res
!= 0x400) {
974 if (callback_res
> 0x400) {
975 ErrorUnknownCallbackResult(ind
->grf_prop
.grffile
->grfid
, CBID_INDUSTRY_WINDOW_MORE_TEXT
, callback_res
);
977 StringID message
= GetGRFStringID(ind
->grf_prop
.grffile
->grfid
, 0xD000 + callback_res
);
978 if (message
!= STR_NULL
&& message
!= STR_UNDEFINED
) {
979 ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
981 StartTextRefStackUsage(ind
->grf_prop
.grffile
, 6);
982 /* Use all the available space left from where we stand up to the
983 * end of the window. We ALSO enlarge the window if needed, so we
984 * can 'go' wild with the bottom of the window. */
985 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, message
, TC_BLACK
);
986 StopTextRefStackUsage();
992 if (!i
->text
.empty()) {
993 SetDParamStr(0, i
->text
);
994 ir
.top
+= WidgetDimensions::scaled
.vsep_wide
;
995 ir
.top
= DrawStringMultiLine(ir
.left
, ir
.right
, ir
.top
, UINT16_MAX
, STR_JUST_RAW_STRING
, TC_BLACK
);
998 /* Return required bottom position, the last pixel row plus some padding. */
999 return ir
.top
- 1 + WidgetDimensions::scaled
.framerect
.bottom
;
1002 void SetStringParameters(WidgetID widget
) const override
1004 if (widget
== WID_IV_CAPTION
) SetDParam(0, this->window_number
);
1007 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
1009 if (widget
== WID_IV_INFO
) size
.height
= this->info_height
;
1012 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
1016 Industry
*i
= Industry::Get(this->window_number
);
1017 InfoLine line
= IL_NONE
;
1019 switch (this->editable
) {
1020 case EA_NONE
: break;
1023 if (IsInsideBS(pt
.y
, this->production_offset_y
, this->cheat_line_height
)) line
= IL_MULTIPLIER
;
1027 if (pt
.y
>= this->production_offset_y
) {
1028 int row
= (pt
.y
- this->production_offset_y
) / this->cheat_line_height
;
1029 for (auto itp
= std::begin(i
->produced
); itp
!= std::end(i
->produced
); ++itp
) {
1030 if (!IsValidCargoID(itp
->cargo
)) continue;
1033 line
= (InfoLine
)(IL_RATE1
+ (itp
- std::begin(i
->produced
)));
1040 if (line
== IL_NONE
) return;
1042 bool rtl
= _current_text_dir
== TD_RTL
;
1043 Rect r
= this->GetWidget
<NWidgetBase
>(widget
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.framerect
).Indent(this->cargo_icon_size
.width
+ WidgetDimensions::scaled
.hsep_normal
, rtl
);
1045 if (r
.WithWidth(SETTING_BUTTON_WIDTH
, rtl
).Contains(pt
)) {
1046 /* Clicked buttons, decrease or increase production */
1047 bool decrease
= r
.WithWidth(SETTING_BUTTON_WIDTH
/ 2, rtl
).Contains(pt
);
1048 switch (this->editable
) {
1051 if (i
->prod_level
<= PRODLEVEL_MINIMUM
) return;
1052 i
->prod_level
= static_cast<uint8_t>(std::max
<uint
>(i
->prod_level
/ 2, PRODLEVEL_MINIMUM
));
1054 if (i
->prod_level
>= PRODLEVEL_MAXIMUM
) return;
1055 i
->prod_level
= static_cast<uint8_t>(std::min
<uint
>(i
->prod_level
* 2, PRODLEVEL_MAXIMUM
));
1061 if (i
->produced
[line
- IL_RATE1
].rate
<= 0) return;
1062 i
->produced
[line
- IL_RATE1
].rate
= std::max(i
->produced
[line
- IL_RATE1
].rate
/ 2, 0);
1064 if (i
->produced
[line
- IL_RATE1
].rate
>= 255) return;
1065 /* a zero production industry is unlikely to give anything but zero, so push it a little bit */
1066 int new_prod
= i
->produced
[line
- IL_RATE1
].rate
== 0 ? 1 : i
->produced
[line
- IL_RATE1
].rate
* 2;
1067 i
->produced
[line
- IL_RATE1
].rate
= ClampTo
<uint8_t>(new_prod
);
1071 default: NOT_REACHED();
1074 UpdateIndustryProduction(i
);
1077 this->clicked_line
= line
;
1078 this->clicked_button
= (decrease
^ rtl
) ? 1 : 2;
1079 } else if (r
.Indent(SETTING_BUTTON_WIDTH
+ WidgetDimensions::scaled
.hsep_normal
, rtl
).Contains(pt
)) {
1080 /* clicked the text */
1081 this->editbox_line
= line
;
1082 switch (this->editable
) {
1084 SetDParam(0, RoundDivSU(i
->prod_level
* 100, PRODLEVEL_DEFAULT
));
1085 ShowQueryString(STR_JUST_INT
, STR_CONFIG_GAME_PRODUCTION_LEVEL
, 10, this, CS_ALPHANUMERAL
, QSF_NONE
);
1089 SetDParam(0, i
->produced
[line
- IL_RATE1
].rate
* 8);
1090 ShowQueryString(STR_JUST_INT
, STR_CONFIG_GAME_PRODUCTION
, 10, this, CS_ALPHANUMERAL
, QSF_NONE
);
1093 default: NOT_REACHED();
1100 Industry
*i
= Industry::Get(this->window_number
);
1101 if (_ctrl_pressed
) {
1102 ShowExtraViewportWindow(i
->location
.GetCenterTile());
1104 ScrollMainWindowToTile(i
->location
.GetCenterTile());
1109 case WID_IV_DISPLAY
: {
1110 Industry
*i
= Industry::Get(this->window_number
);
1111 ShowIndustryCargoesWindow(i
->type
);
1117 void OnTimeout() override
1119 this->clicked_line
= IL_NONE
;
1120 this->clicked_button
= 0;
1124 void OnResize() override
1126 if (this->viewport
!= nullptr) {
1127 NWidgetViewport
*nvp
= this->GetWidget
<NWidgetViewport
>(WID_IV_VIEWPORT
);
1128 nvp
->UpdateViewportCoordinates(this);
1130 ScrollWindowToTile(Industry::Get(this->window_number
)->location
.GetCenterTile(), this, true); // Re-center viewport.
1134 void OnMouseWheel(int wheel
) override
1136 if (_settings_client
.gui
.scrollwheel_scrolling
!= SWS_OFF
) {
1137 DoZoomInOutWindow(wheel
< 0 ? ZOOM_IN
: ZOOM_OUT
, this);
1141 void OnQueryTextFinished(std::optional
<std::string
> str
) override
1143 if (!str
.has_value() || str
->empty()) return;
1145 Industry
*i
= Industry::Get(this->window_number
);
1146 uint value
= atoi(str
->c_str());
1147 switch (this->editbox_line
) {
1148 case IL_NONE
: NOT_REACHED();
1151 i
->prod_level
= ClampU(RoundDivSU(value
* PRODLEVEL_DEFAULT
, 100), PRODLEVEL_MINIMUM
, PRODLEVEL_MAXIMUM
);
1155 i
->produced
[this->editbox_line
- IL_RATE1
].rate
= ClampU(RoundDivSU(value
, 8), 0, 255);
1158 UpdateIndustryProduction(i
);
1163 * Some data on this window has become invalid.
1164 * @param data Information about the changed data.
1165 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
1167 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
1169 if (!gui_scope
) return;
1170 const Industry
*i
= Industry::Get(this->window_number
);
1171 if (IsProductionAlterable(i
)) {
1172 const IndustrySpec
*ind
= GetIndustrySpec(i
->type
);
1173 this->editable
= ind
->UsesOriginalEconomy() ? EA_MULTIPLIER
: EA_RATE
;
1175 this->editable
= EA_NONE
;
1179 bool IsNewGRFInspectable() const override
1181 return ::IsNewGRFInspectable(GSF_INDUSTRIES
, this->window_number
);
1184 void ShowNewGRFInspectWindow() const override
1186 ::ShowNewGRFInspectWindow(GSF_INDUSTRIES
, this->window_number
);
1190 static void UpdateIndustryProduction(Industry
*i
)
1192 const IndustrySpec
*indspec
= GetIndustrySpec(i
->type
);
1193 if (indspec
->UsesOriginalEconomy()) i
->RecomputeProductionMultipliers();
1195 for (auto &p
: i
->produced
) {
1196 if (IsValidCargoID(p
.cargo
)) {
1197 p
.history
[LAST_MONTH
].production
= ScaleByCargoScale(8 * p
.rate
, false);
1202 /** Widget definition of the view industry gui */
1203 static constexpr NWidgetPart _nested_industry_view_widgets
[] = {
1204 NWidget(NWID_HORIZONTAL
),
1205 NWidget(WWT_CLOSEBOX
, COLOUR_CREAM
),
1206 NWidget(WWT_CAPTION
, COLOUR_CREAM
, WID_IV_CAPTION
), SetDataTip(STR_INDUSTRY_VIEW_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1207 NWidget(WWT_PUSHIMGBTN
, COLOUR_CREAM
, WID_IV_GOTO
), SetAspect(WidgetDimensions::ASPECT_LOCATION
), SetDataTip(SPR_GOTO_LOCATION
, STR_INDUSTRY_VIEW_LOCATION_TOOLTIP
),
1208 NWidget(WWT_DEBUGBOX
, COLOUR_CREAM
),
1209 NWidget(WWT_SHADEBOX
, COLOUR_CREAM
),
1210 NWidget(WWT_DEFSIZEBOX
, COLOUR_CREAM
),
1211 NWidget(WWT_STICKYBOX
, COLOUR_CREAM
),
1213 NWidget(WWT_PANEL
, COLOUR_CREAM
),
1214 NWidget(WWT_INSET
, COLOUR_CREAM
), SetPadding(2, 2, 2, 2),
1215 NWidget(NWID_VIEWPORT
, INVALID_COLOUR
, WID_IV_VIEWPORT
), SetMinimalSize(254, 86), SetFill(1, 0), SetResize(1, 1),
1218 NWidget(WWT_PANEL
, COLOUR_CREAM
, WID_IV_INFO
), SetMinimalSize(260, 0), SetMinimalTextLines(2, WidgetDimensions::unscaled
.framerect
.Vertical()), SetResize(1, 0),
1220 NWidget(NWID_HORIZONTAL
),
1221 NWidget(WWT_PUSHTXTBTN
, COLOUR_CREAM
, WID_IV_DISPLAY
), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_INDUSTRY_DISPLAY_CHAIN
, STR_INDUSTRY_DISPLAY_CHAIN_TOOLTIP
),
1222 NWidget(WWT_RESIZEBOX
, COLOUR_CREAM
),
1226 /** Window definition of the view industry gui */
1227 static WindowDesc
_industry_view_desc(
1228 WDP_AUTO
, "view_industry", 260, 120,
1229 WC_INDUSTRY_VIEW
, WC_NONE
,
1231 _nested_industry_view_widgets
1234 void ShowIndustryViewWindow(int industry
)
1236 AllocateWindowDescFront
<IndustryViewWindow
>(_industry_view_desc
, industry
);
1239 /** Widget definition of the industry directory gui */
1240 static constexpr NWidgetPart _nested_industry_directory_widgets
[] = {
1241 NWidget(NWID_HORIZONTAL
),
1242 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
1243 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_ID_CAPTION
), SetDataTip(STR_INDUSTRY_DIRECTORY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1244 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
1245 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
1246 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
1248 NWidget(NWID_HORIZONTAL
),
1249 NWidget(NWID_VERTICAL
),
1250 NWidget(NWID_HORIZONTAL
),
1251 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_ID_DROPDOWN_ORDER
), SetDataTip(STR_BUTTON_SORT_BY
, STR_TOOLTIP_SORT_ORDER
),
1252 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_ID_DROPDOWN_CRITERIA
), SetDataTip(STR_JUST_STRING
, STR_TOOLTIP_SORT_CRITERIA
),
1253 NWidget(WWT_EDITBOX
, COLOUR_BROWN
, WID_ID_FILTER
), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_LIST_FILTER_OSKTITLE
, STR_LIST_FILTER_TOOLTIP
),
1255 NWidget(NWID_HORIZONTAL
),
1256 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_ID_FILTER_BY_ACC_CARGO
), SetMinimalSize(225, 12), SetFill(0, 1), SetDataTip(STR_INDUSTRY_DIRECTORY_ACCEPTED_CARGO_FILTER
, STR_TOOLTIP_FILTER_CRITERIA
),
1257 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_ID_FILTER_BY_PROD_CARGO
), SetMinimalSize(225, 12), SetFill(0, 1), SetDataTip(STR_INDUSTRY_DIRECTORY_PRODUCED_CARGO_FILTER
, STR_TOOLTIP_FILTER_CRITERIA
),
1258 NWidget(WWT_PANEL
, COLOUR_BROWN
), SetResize(1, 0), EndContainer(),
1260 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_ID_INDUSTRY_LIST
), SetDataTip(0x0, STR_INDUSTRY_DIRECTORY_LIST_CAPTION
), SetResize(1, 1), SetScrollbar(WID_ID_VSCROLLBAR
),
1263 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_ID_VSCROLLBAR
),
1265 NWidget(NWID_HORIZONTAL
),
1266 NWidget(NWID_HSCROLLBAR
, COLOUR_BROWN
, WID_ID_HSCROLLBAR
),
1267 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
1271 typedef GUIList
<const Industry
*, const CargoID
&, const std::pair
<CargoID
, CargoID
> &> GUIIndustryList
;
1273 /** Cargo filter functions */
1275 * Check whether an industry accepts and produces a certain cargo pair.
1276 * @param industry The industry whose cargoes will being checked.
1277 * @param cargoes The accepted and produced cargo pair to look for.
1278 * @return bool Whether the given cargoes accepted and produced by the industry.
1280 static bool CargoFilter(const Industry
* const *industry
, const std::pair
<CargoID
, CargoID
> &cargoes
)
1282 auto accepted_cargo
= cargoes
.first
;
1283 auto produced_cargo
= cargoes
.second
;
1285 bool accepted_cargo_matches
;
1287 switch (accepted_cargo
) {
1288 case CargoFilterCriteria::CF_ANY
:
1289 accepted_cargo_matches
= true;
1292 case CargoFilterCriteria::CF_NONE
:
1293 accepted_cargo_matches
= !(*industry
)->IsCargoAccepted();
1297 accepted_cargo_matches
= (*industry
)->IsCargoAccepted(accepted_cargo
);
1301 bool produced_cargo_matches
;
1303 switch (produced_cargo
) {
1304 case CargoFilterCriteria::CF_ANY
:
1305 produced_cargo_matches
= true;
1308 case CargoFilterCriteria::CF_NONE
:
1309 produced_cargo_matches
= !(*industry
)->IsCargoProduced();
1313 produced_cargo_matches
= (*industry
)->IsCargoProduced(produced_cargo
);
1317 return accepted_cargo_matches
&& produced_cargo_matches
;
1320 static GUIIndustryList::FilterFunction
* const _industry_filter_funcs
[] = { &CargoFilter
};
1322 /** Enum referring to the Hotkeys in the industry directory window */
1323 enum IndustryDirectoryHotkeys
{
1324 IDHK_FOCUS_FILTER_BOX
, ///< Focus the filter box
1327 * The list of industries.
1329 class IndustryDirectoryWindow
: public Window
{
1331 /* Runtime saved values */
1332 static Listing last_sorting
;
1334 /* Constants for sorting industries */
1335 static inline const StringID sorter_names
[] = {
1338 STR_SORT_BY_PRODUCTION
,
1339 STR_SORT_BY_TRANSPORTED
,
1341 static const std::initializer_list
<GUIIndustryList::SortFunction
* const> sorter_funcs
;
1343 GUIIndustryList industries
{IndustryDirectoryWindow::produced_cargo_filter
};
1347 CargoID produced_cargo_filter_criteria
; ///< Selected produced cargo filter index
1348 CargoID accepted_cargo_filter_criteria
; ///< Selected accepted cargo filter index
1349 static CargoID produced_cargo_filter
;
1351 const int MAX_FILTER_LENGTH
= 16; ///< The max length of the filter, in chars
1352 StringFilter string_filter
; ///< Filter for industries
1353 QueryString industry_editbox
; ///< Filter editbox
1355 enum class SorterType
: uint8_t {
1356 ByName
, ///< Sorter type to sort by name
1357 ByType
, ///< Sorter type to sort by type
1358 ByProduction
, ///< Sorter type to sort by production amount
1359 ByTransported
, ///< Sorter type to sort by transported percentage
1363 * Set produced cargo filter for the industry list.
1364 * @param cid The cargo to be set
1366 void SetProducedCargoFilter(CargoID cid
)
1368 if (this->produced_cargo_filter_criteria
!= cid
) {
1369 this->produced_cargo_filter_criteria
= cid
;
1370 /* deactivate filter if criteria is 'Show All', activate it otherwise */
1371 bool is_filtering_necessary
= this->produced_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
|| this->accepted_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
;
1373 this->industries
.SetFilterState(is_filtering_necessary
);
1374 this->industries
.SetFilterType(0);
1375 this->industries
.ForceRebuild();
1380 * Set accepted cargo filter for the industry list.
1381 * @param index The cargo to be set
1383 void SetAcceptedCargoFilter(CargoID cid
)
1385 if (this->accepted_cargo_filter_criteria
!= cid
) {
1386 this->accepted_cargo_filter_criteria
= cid
;
1387 /* deactivate filter if criteria is 'Show All', activate it otherwise */
1388 bool is_filtering_necessary
= this->produced_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
|| this->accepted_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
;
1390 this->industries
.SetFilterState(is_filtering_necessary
);
1391 this->industries
.SetFilterType(0);
1392 this->industries
.ForceRebuild();
1396 StringID
GetCargoFilterLabel(CargoID cid
) const
1399 case CargoFilterCriteria::CF_ANY
: return STR_INDUSTRY_DIRECTORY_FILTER_ALL_TYPES
;
1400 case CargoFilterCriteria::CF_NONE
: return STR_INDUSTRY_DIRECTORY_FILTER_NONE
;
1401 default: return CargoSpec::Get(cid
)->name
;
1406 * Populate the filter list and set the cargo filter criteria.
1408 void SetCargoFilterArray()
1410 this->produced_cargo_filter_criteria
= CargoFilterCriteria::CF_ANY
;
1411 this->accepted_cargo_filter_criteria
= CargoFilterCriteria::CF_ANY
;
1413 this->industries
.SetFilterFuncs(_industry_filter_funcs
);
1415 bool is_filtering_necessary
= this->produced_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
|| this->accepted_cargo_filter_criteria
!= CargoFilterCriteria::CF_ANY
;
1417 this->industries
.SetFilterState(is_filtering_necessary
);
1421 * Get the width needed to draw the longest industry line.
1422 * @return Returns width of the longest industry line, including padding.
1424 uint
GetIndustryListWidth() const
1427 for (const Industry
*i
: this->industries
) {
1428 width
= std::max(width
, GetStringBoundingBox(this->GetIndustryString(i
)).width
);
1430 return width
+ WidgetDimensions::scaled
.framerect
.Horizontal();
1433 /** (Re)Build industries list */
1434 void BuildSortIndustriesList()
1436 if (this->industries
.NeedRebuild()) {
1437 this->industries
.clear();
1438 this->industries
.reserve(Industry::GetNumItems());
1440 for (const Industry
*i
: Industry::Iterate()) {
1441 if (this->string_filter
.IsEmpty()) {
1442 this->industries
.push_back(i
);
1445 this->string_filter
.ResetState();
1446 this->string_filter
.AddLine(i
->GetCachedName());
1447 if (this->string_filter
.GetState()) this->industries
.push_back(i
);
1450 this->industries
.RebuildDone();
1452 auto filter
= std::make_pair(this->accepted_cargo_filter_criteria
, this->produced_cargo_filter_criteria
);
1454 this->industries
.Filter(filter
);
1456 this->hscroll
->SetCount(this->GetIndustryListWidth());
1457 this->vscroll
->SetCount(this->industries
.size()); // Update scrollbar as well.
1460 IndustryDirectoryWindow::produced_cargo_filter
= this->produced_cargo_filter_criteria
;
1461 this->industries
.Sort();
1467 * Returns percents of cargo transported if industry produces this cargo, else -1
1469 * @param i industry to check
1470 * @param id cargo slot
1471 * @return percents of cargo transported, or -1 if industry doesn't use this cargo slot
1473 static inline int GetCargoTransportedPercentsIfValid(const Industry::ProducedCargo
&p
)
1475 if (!IsValidCargoID(p
.cargo
)) return -1;
1476 return ToPercent8(p
.history
[LAST_MONTH
].PctTransported());
1480 * Returns value representing industry's transported cargo
1481 * percentage for industry sorting
1483 * @param i industry to check
1484 * @return value used for sorting
1486 static int GetCargoTransportedSortValue(const Industry
*i
)
1488 CargoID filter
= IndustryDirectoryWindow::produced_cargo_filter
;
1489 if (filter
== CargoFilterCriteria::CF_NONE
) return 0;
1491 int percentage
= 0, produced_cargo_count
= 0;
1492 for (const auto &p
: i
->produced
) {
1493 if (filter
== CargoFilterCriteria::CF_ANY
) {
1494 int transported
= GetCargoTransportedPercentsIfValid(p
);
1495 if (transported
!= -1) {
1496 produced_cargo_count
++;
1497 percentage
+= transported
;
1499 if (produced_cargo_count
== 0 && &p
== &i
->produced
.back() && percentage
== 0) {
1502 } else if (filter
== p
.cargo
) {
1503 return GetCargoTransportedPercentsIfValid(p
);
1507 if (produced_cargo_count
== 0) return percentage
;
1508 return percentage
/ produced_cargo_count
;
1511 /** Sort industries by name */
1512 static bool IndustryNameSorter(const Industry
* const &a
, const Industry
* const &b
, const CargoID
&)
1514 int r
= StrNaturalCompare(a
->GetCachedName(), b
->GetCachedName()); // Sort by name (natural sorting).
1515 if (r
== 0) return a
->index
< b
->index
;
1519 /** Sort industries by type and name */
1520 static bool IndustryTypeSorter(const Industry
* const &a
, const Industry
* const &b
, const CargoID
&filter
)
1523 while (it_a
!= NUM_INDUSTRYTYPES
&& a
->type
!= _sorted_industry_types
[it_a
]) it_a
++;
1525 while (it_b
!= NUM_INDUSTRYTYPES
&& b
->type
!= _sorted_industry_types
[it_b
]) it_b
++;
1526 int r
= it_a
- it_b
;
1527 return (r
== 0) ? IndustryNameSorter(a
, b
, filter
) : r
< 0;
1530 /** Sort industries by production and name */
1531 static bool IndustryProductionSorter(const Industry
* const &a
, const Industry
* const &b
, const CargoID
&filter
)
1533 if (filter
== CargoFilterCriteria::CF_NONE
) return IndustryTypeSorter(a
, b
, filter
);
1535 uint prod_a
= 0, prod_b
= 0;
1536 if (filter
== CargoFilterCriteria::CF_ANY
) {
1537 for (const auto &pa
: a
->produced
) {
1538 if (IsValidCargoID(pa
.cargo
)) prod_a
+= pa
.history
[LAST_MONTH
].production
;
1540 for (const auto &pb
: b
->produced
) {
1541 if (IsValidCargoID(pb
.cargo
)) prod_b
+= pb
.history
[LAST_MONTH
].production
;
1544 if (auto ita
= a
->GetCargoProduced(filter
); ita
!= std::end(a
->produced
)) prod_a
= ita
->history
[LAST_MONTH
].production
;
1545 if (auto itb
= b
->GetCargoProduced(filter
); itb
!= std::end(b
->produced
)) prod_b
= itb
->history
[LAST_MONTH
].production
;
1547 int r
= prod_a
- prod_b
;
1549 return (r
== 0) ? IndustryTypeSorter(a
, b
, filter
) : r
< 0;
1552 /** Sort industries by transported cargo and name */
1553 static bool IndustryTransportedCargoSorter(const Industry
* const &a
, const Industry
* const &b
, const CargoID
&filter
)
1555 int r
= GetCargoTransportedSortValue(a
) - GetCargoTransportedSortValue(b
);
1556 return (r
== 0) ? IndustryNameSorter(a
, b
, filter
) : r
< 0;
1560 * Get the StringID to draw and set the appropriate DParams.
1561 * @param i the industry to get the StringID of.
1562 * @return the StringID.
1564 StringID
GetIndustryString(const Industry
*i
) const
1566 const IndustrySpec
*indsp
= GetIndustrySpec(i
->type
);
1570 SetDParam(p
++, i
->index
);
1572 /* Get industry productions (CargoID, production, suffix, transported) */
1574 CargoID cargo_id
; ///< Cargo ID.
1575 uint16_t production
; ///< Production last month.
1576 uint transported
; ///< Percent transported last month.
1577 std::string suffix
; ///< Cargo suffix.
1579 CargoInfo(CargoID cargo_id
, uint16_t production
, uint transported
, std::string
&&suffix
) : cargo_id(cargo_id
), production(production
), transported(transported
), suffix(std::move(suffix
)) {}
1581 std::vector
<CargoInfo
> cargos
;
1583 for (auto itp
= std::begin(i
->produced
); itp
!= std::end(i
->produced
); ++itp
) {
1584 if (!IsValidCargoID(itp
->cargo
)) continue;
1585 CargoSuffix cargo_suffix
;
1586 GetCargoSuffix(CARGOSUFFIX_OUT
, CST_DIR
, i
, i
->type
, indsp
, itp
->cargo
, itp
- std::begin(i
->produced
), cargo_suffix
);
1587 cargos
.emplace_back(itp
->cargo
, itp
->history
[LAST_MONTH
].production
, ToPercent8(itp
->history
[LAST_MONTH
].PctTransported()), std::move(cargo_suffix
.text
));
1590 switch (static_cast<IndustryDirectoryWindow::SorterType
>(this->industries
.SortType())) {
1591 case IndustryDirectoryWindow::SorterType::ByName
:
1592 case IndustryDirectoryWindow::SorterType::ByType
:
1593 case IndustryDirectoryWindow::SorterType::ByProduction
:
1594 /* Sort by descending production, then descending transported */
1595 std::sort(cargos
.begin(), cargos
.end(), [](const CargoInfo
&a
, const CargoInfo
&b
) {
1596 if (a
.production
!= b
.production
) return a
.production
> b
.production
;
1597 return a
.transported
> b
.transported
;
1601 case IndustryDirectoryWindow::SorterType::ByTransported
:
1602 /* Sort by descending transported, then descending production */
1603 std::sort(cargos
.begin(), cargos
.end(), [](const CargoInfo
&a
, const CargoInfo
&b
) {
1604 if (a
.transported
!= b
.transported
) return a
.transported
> b
.transported
;
1605 return a
.production
> b
.production
;
1610 /* If the produced cargo filter is active then move the filtered cargo to the beginning of the list,
1611 * because this is the one the player interested in, and that way it is not hidden in the 'n' more cargos */
1612 const CargoID cid
= this->produced_cargo_filter_criteria
;
1613 if (cid
!= CargoFilterCriteria::CF_ANY
&& cid
!= CargoFilterCriteria::CF_NONE
) {
1614 auto filtered_ci
= std::find_if(cargos
.begin(), cargos
.end(), [cid
](const CargoInfo
&ci
) -> bool {
1615 return ci
.cargo_id
== cid
;
1617 if (filtered_ci
!= cargos
.end()) {
1618 std::rotate(cargos
.begin(), filtered_ci
, filtered_ci
+ 1);
1622 /* Display first 3 cargos */
1623 for (size_t j
= 0; j
< std::min
<size_t>(3, cargos
.size()); j
++) {
1624 CargoInfo
&ci
= cargos
[j
];
1625 SetDParam(p
++, STR_INDUSTRY_DIRECTORY_ITEM_INFO
);
1626 SetDParam(p
++, ci
.cargo_id
);
1627 SetDParam(p
++, ci
.production
);
1628 SetDParamStr(p
++, std::move(ci
.suffix
));
1629 SetDParam(p
++, ci
.transported
);
1632 /* Undisplayed cargos if any */
1633 SetDParam(p
++, cargos
.size() - 3);
1635 /* Drawing the right string */
1636 switch (cargos
.size()) {
1637 case 0: return STR_INDUSTRY_DIRECTORY_ITEM_NOPROD
;
1638 case 1: return STR_INDUSTRY_DIRECTORY_ITEM_PROD1
;
1639 case 2: return STR_INDUSTRY_DIRECTORY_ITEM_PROD2
;
1640 case 3: return STR_INDUSTRY_DIRECTORY_ITEM_PROD3
;
1641 default: return STR_INDUSTRY_DIRECTORY_ITEM_PRODMORE
;
1646 IndustryDirectoryWindow(WindowDesc
&desc
, WindowNumber
) : Window(desc
), industry_editbox(MAX_FILTER_LENGTH
* MAX_CHAR_LENGTH
, MAX_FILTER_LENGTH
)
1648 this->CreateNestedTree();
1649 this->vscroll
= this->GetScrollbar(WID_ID_VSCROLLBAR
);
1650 this->hscroll
= this->GetScrollbar(WID_ID_HSCROLLBAR
);
1652 this->industries
.SetListing(this->last_sorting
);
1653 this->industries
.SetSortFuncs(IndustryDirectoryWindow::sorter_funcs
);
1654 this->industries
.ForceRebuild();
1656 this->FinishInitNested(0);
1658 this->BuildSortIndustriesList();
1660 this->querystrings
[WID_ID_FILTER
] = &this->industry_editbox
;
1661 this->industry_editbox
.cancel_button
= QueryString::ACTION_CLEAR
;
1664 ~IndustryDirectoryWindow()
1666 this->last_sorting
= this->industries
.GetListing();
1669 void OnInit() override
1671 this->SetCargoFilterArray();
1674 void SetStringParameters(WidgetID widget
) const override
1677 case WID_ID_CAPTION
:
1678 SetDParam(0, this->vscroll
->GetCount());
1679 SetDParam(1, Industry::GetNumItems());
1682 case WID_ID_DROPDOWN_CRITERIA
:
1683 SetDParam(0, IndustryDirectoryWindow::sorter_names
[this->industries
.SortType()]);
1686 case WID_ID_FILTER_BY_ACC_CARGO
:
1687 SetDParam(0, this->GetCargoFilterLabel(this->accepted_cargo_filter_criteria
));
1690 case WID_ID_FILTER_BY_PROD_CARGO
:
1691 SetDParam(0, this->GetCargoFilterLabel(this->produced_cargo_filter_criteria
));
1696 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
1699 case WID_ID_DROPDOWN_ORDER
:
1700 this->DrawSortButtonState(widget
, this->industries
.IsDescSortOrder() ? SBS_DOWN
: SBS_UP
);
1703 case WID_ID_INDUSTRY_LIST
: {
1704 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.framerect
);
1706 /* Setup a clipping rectangle... */
1707 DrawPixelInfo tmp_dpi
;
1708 if (!FillDrawPixelInfo(&tmp_dpi
, ir
)) return;
1709 /* ...but keep coordinates relative to the window. */
1710 tmp_dpi
.left
+= ir
.left
;
1711 tmp_dpi
.top
+= ir
.top
;
1713 AutoRestoreBackup
dpi_backup(_cur_dpi
, &tmp_dpi
);
1715 ir
.left
-= this->hscroll
->GetPosition();
1716 ir
.right
+= this->hscroll
->GetCapacity() - this->hscroll
->GetPosition();
1718 if (this->industries
.empty()) {
1719 DrawString(ir
, STR_INDUSTRY_DIRECTORY_NONE
);
1722 const CargoID acf_cid
= this->accepted_cargo_filter_criteria
;
1723 auto [first
, last
] = this->vscroll
->GetVisibleRangeIterators(this->industries
);
1724 for (auto it
= first
; it
!= last
; ++it
) {
1725 TextColour tc
= TC_FROMSTRING
;
1726 if (acf_cid
!= CargoFilterCriteria::CF_ANY
&& acf_cid
!= CargoFilterCriteria::CF_NONE
) {
1727 Industry
*ind
= const_cast<Industry
*>(*it
);
1728 if (IndustryTemporarilyRefusesCargo(ind
, acf_cid
)) {
1729 tc
= TC_GREY
| TC_FORCED
;
1732 DrawString(ir
, this->GetIndustryString(*it
), tc
);
1734 ir
.top
+= this->resize
.step_height
;
1741 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
1744 case WID_ID_DROPDOWN_ORDER
: {
1745 Dimension d
= GetStringBoundingBox(this->GetWidget
<NWidgetCore
>(widget
)->widget_data
);
1746 d
.width
+= padding
.width
+ Window::SortButtonWidth() * 2; // Doubled since the string is centred and it also looks better.
1747 d
.height
+= padding
.height
;
1748 size
= maxdim(size
, d
);
1752 case WID_ID_DROPDOWN_CRITERIA
: {
1753 Dimension d
= GetStringListBoundingBox(IndustryDirectoryWindow::sorter_names
);
1754 d
.width
+= padding
.width
;
1755 d
.height
+= padding
.height
;
1756 size
= maxdim(size
, d
);
1760 case WID_ID_INDUSTRY_LIST
: {
1761 Dimension d
= GetStringBoundingBox(STR_INDUSTRY_DIRECTORY_NONE
);
1762 resize
.height
= d
.height
;
1764 d
.width
+= padding
.width
;
1765 d
.height
+= padding
.height
;
1766 size
= maxdim(size
, d
);
1772 DropDownList
BuildCargoDropDownList() const
1776 /* Add item for disabling filtering. */
1777 list
.push_back(MakeDropDownListStringItem(this->GetCargoFilterLabel(CargoFilterCriteria::CF_ANY
), CargoFilterCriteria::CF_ANY
));
1778 /* Add item for industries not producing anything, e.g. power plants */
1779 list
.push_back(MakeDropDownListStringItem(this->GetCargoFilterLabel(CargoFilterCriteria::CF_NONE
), CargoFilterCriteria::CF_NONE
));
1782 Dimension d
= GetLargestCargoIconSize();
1783 for (const CargoSpec
*cs
: _sorted_standard_cargo_specs
) {
1784 list
.push_back(MakeDropDownListIconItem(d
, cs
->GetCargoIcon(), PAL_NONE
, cs
->name
, cs
->Index()));
1790 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
1793 case WID_ID_DROPDOWN_ORDER
:
1794 this->industries
.ToggleSortOrder();
1798 case WID_ID_DROPDOWN_CRITERIA
:
1799 ShowDropDownMenu(this, IndustryDirectoryWindow::sorter_names
, this->industries
.SortType(), WID_ID_DROPDOWN_CRITERIA
, 0, 0);
1802 case WID_ID_FILTER_BY_ACC_CARGO
: // Cargo filter dropdown
1803 ShowDropDownList(this, this->BuildCargoDropDownList(), this->accepted_cargo_filter_criteria
, widget
);
1806 case WID_ID_FILTER_BY_PROD_CARGO
: // Cargo filter dropdown
1807 ShowDropDownList(this, this->BuildCargoDropDownList(), this->produced_cargo_filter_criteria
, widget
);
1810 case WID_ID_INDUSTRY_LIST
: {
1811 auto it
= this->vscroll
->GetScrolledItemFromWidget(this->industries
, pt
.y
, this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.top
);
1812 if (it
!= this->industries
.end()) {
1813 if (_ctrl_pressed
) {
1814 ShowExtraViewportWindow((*it
)->location
.tile
);
1816 ScrollMainWindowToTile((*it
)->location
.tile
);
1824 void OnDropdownSelect(WidgetID widget
, int index
) override
1827 case WID_ID_DROPDOWN_CRITERIA
: {
1828 if (this->industries
.SortType() != index
) {
1829 this->industries
.SetSortType(index
);
1830 this->BuildSortIndustriesList();
1835 case WID_ID_FILTER_BY_ACC_CARGO
: {
1836 this->SetAcceptedCargoFilter(index
);
1837 this->BuildSortIndustriesList();
1841 case WID_ID_FILTER_BY_PROD_CARGO
: {
1842 this->SetProducedCargoFilter(index
);
1843 this->BuildSortIndustriesList();
1849 void OnResize() override
1851 this->vscroll
->SetCapacityFromWidget(this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.Vertical());
1852 this->hscroll
->SetCapacityFromWidget(this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.Horizontal());
1855 void OnEditboxChanged(WidgetID wid
) override
1857 if (wid
== WID_ID_FILTER
) {
1858 this->string_filter
.SetFilterTerm(this->industry_editbox
.text
.buf
);
1859 this->InvalidateData(IDIWD_FORCE_REBUILD
);
1863 void OnPaint() override
1865 if (this->industries
.NeedRebuild()) this->BuildSortIndustriesList();
1866 this->DrawWidgets();
1869 /** Rebuild the industry list on a regular interval. */
1870 IntervalTimer
<TimerWindow
> rebuild_interval
= {std::chrono::seconds(3), [this](auto) {
1871 this->industries
.ForceResort();
1872 this->BuildSortIndustriesList();
1876 * Some data on this window has become invalid.
1877 * @param data Information about the changed data.
1878 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
1880 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
1883 case IDIWD_FORCE_REBUILD
:
1884 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
1885 this->industries
.ForceRebuild();
1888 case IDIWD_PRODUCTION_CHANGE
:
1889 if (this->industries
.SortType() == 2) this->industries
.ForceResort();
1893 this->industries
.ForceResort();
1898 EventState
OnHotkey(int hotkey
) override
1901 case IDHK_FOCUS_FILTER_BOX
:
1902 this->SetFocusedWidget(WID_ID_FILTER
);
1903 SetFocusedWindow(this); // The user has asked to give focus to the text box, so make sure this window is focused.
1906 return ES_NOT_HANDLED
;
1911 static inline HotkeyList hotkeys
{"industrydirectory", {
1912 Hotkey('F', "focus_filter_box", IDHK_FOCUS_FILTER_BOX
),
1916 Listing
IndustryDirectoryWindow::last_sorting
= {false, 0};
1918 /* Available station sorting functions. */
1919 const std::initializer_list
<GUIIndustryList::SortFunction
* const> IndustryDirectoryWindow::sorter_funcs
= {
1920 &IndustryNameSorter
,
1921 &IndustryTypeSorter
,
1922 &IndustryProductionSorter
,
1923 &IndustryTransportedCargoSorter
1926 CargoID
IndustryDirectoryWindow::produced_cargo_filter
= CargoFilterCriteria::CF_ANY
;
1929 /** Window definition of the industry directory gui */
1930 static WindowDesc
_industry_directory_desc(
1931 WDP_AUTO
, "list_industries", 428, 190,
1932 WC_INDUSTRY_DIRECTORY
, WC_NONE
,
1934 _nested_industry_directory_widgets
,
1935 &IndustryDirectoryWindow::hotkeys
1938 void ShowIndustryDirectory()
1940 AllocateWindowDescFront
<IndustryDirectoryWindow
>(_industry_directory_desc
, 0);
1943 /** Widgets of the industry cargoes window. */
1944 static constexpr NWidgetPart _nested_industry_cargoes_widgets
[] = {
1945 NWidget(NWID_HORIZONTAL
),
1946 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
1947 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_IC_CAPTION
), SetDataTip(STR_INDUSTRY_CARGOES_INDUSTRY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1948 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
1949 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
1950 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
1952 NWidget(NWID_HORIZONTAL
),
1953 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_IC_PANEL
), SetResize(1, 10), SetScrollbar(WID_IC_SCROLLBAR
), EndContainer(),
1954 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_IC_SCROLLBAR
),
1956 NWidget(NWID_HORIZONTAL
),
1957 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_IC_NOTIFY
),
1958 SetDataTip(STR_INDUSTRY_CARGOES_NOTIFY_SMALLMAP
, STR_INDUSTRY_CARGOES_NOTIFY_SMALLMAP_TOOLTIP
),
1959 NWidget(WWT_PANEL
, COLOUR_BROWN
), SetFill(1, 0), SetResize(0, 0), EndContainer(),
1960 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_IC_IND_DROPDOWN
), SetFill(0, 0), SetResize(0, 0),
1961 SetDataTip(STR_INDUSTRY_CARGOES_SELECT_INDUSTRY
, STR_INDUSTRY_CARGOES_SELECT_INDUSTRY_TOOLTIP
),
1962 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_IC_CARGO_DROPDOWN
), SetFill(0, 0), SetResize(0, 0),
1963 SetDataTip(STR_INDUSTRY_CARGOES_SELECT_CARGO
, STR_INDUSTRY_CARGOES_SELECT_CARGO_TOOLTIP
),
1964 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
1968 /** Window description for the industry cargoes window. */
1969 static WindowDesc
_industry_cargoes_desc(
1970 WDP_AUTO
, "industry_cargoes", 300, 210,
1971 WC_INDUSTRY_CARGOES
, WC_NONE
,
1973 _nested_industry_cargoes_widgets
1976 /** Available types of field. */
1977 enum CargoesFieldType
{
1978 CFT_EMPTY
, ///< Empty field.
1979 CFT_SMALL_EMPTY
, ///< Empty small field (for the header).
1980 CFT_INDUSTRY
, ///< Display industry.
1981 CFT_CARGO
, ///< Display cargo connections.
1982 CFT_CARGO_LABEL
, ///< Display cargo labels.
1983 CFT_HEADER
, ///< Header text.
1986 static const uint MAX_CARGOES
= 16; ///< Maximum number of cargoes carried in a #CFT_CARGO field in #CargoesField.
1988 /** Data about a single field in the #IndustryCargoesWindow panel. */
1989 struct CargoesField
{
1990 static int vert_inter_industry_space
;
1991 static int blob_distance
;
1993 static Dimension legend
;
1994 static Dimension cargo_border
;
1995 static Dimension cargo_line
;
1996 static Dimension cargo_space
;
1997 static Dimension cargo_stub
;
1999 static const int INDUSTRY_LINE_COLOUR
;
2000 static const int CARGO_LINE_COLOUR
;
2002 static int small_height
, normal_height
;
2003 static int cargo_field_width
;
2004 static int industry_width
;
2005 static uint max_cargoes
;
2007 using Cargoes
= uint16_t;
2008 static_assert(std::numeric_limits
<Cargoes
>::digits
>= MAX_CARGOES
);
2010 CargoesFieldType type
; ///< Type of field.
2013 IndustryType ind_type
; ///< Industry type (#NUM_INDUSTRYTYPES means 'houses').
2014 CargoID other_produced
[MAX_CARGOES
]; ///< Cargoes produced but not used in this figure.
2015 CargoID other_accepted
[MAX_CARGOES
]; ///< Cargoes accepted but not used in this figure.
2016 } industry
; ///< Industry data (for #CFT_INDUSTRY).
2018 CargoID vertical_cargoes
[MAX_CARGOES
]; ///< Cargoes running from top to bottom (cargo ID or #INVALID_CARGO).
2019 Cargoes supp_cargoes
; ///< Cargoes in \c vertical_cargoes entering from the left.
2020 Cargoes cust_cargoes
; ///< Cargoes in \c vertical_cargoes leaving to the right.
2021 uint8_t num_cargoes
; ///< Number of cargoes.
2022 uint8_t top_end
; ///< Stop at the top of the vertical cargoes.
2023 uint8_t bottom_end
; ///< Stop at the bottom of the vertical cargoes.
2024 } cargo
; ///< Cargo data (for #CFT_CARGO).
2026 CargoID cargoes
[MAX_CARGOES
]; ///< Cargoes to display (or #INVALID_CARGO).
2027 bool left_align
; ///< Align all cargo texts to the left (else align to the right).
2028 } cargo_label
; ///< Label data (for #CFT_CARGO_LABEL).
2029 StringID header
; ///< Header text (for #CFT_HEADER).
2030 } u
; // Data for each type.
2033 * Make one of the empty fields (#CFT_EMPTY or #CFT_SMALL_EMPTY).
2034 * @param type Type of empty field.
2036 void MakeEmpty(CargoesFieldType type
)
2042 * Make an industry type field.
2043 * @param ind_type Industry type (#NUM_INDUSTRYTYPES means 'houses').
2044 * @note #other_accepted and #other_produced should be filled later.
2046 void MakeIndustry(IndustryType ind_type
)
2048 this->type
= CFT_INDUSTRY
;
2049 this->u
.industry
.ind_type
= ind_type
;
2050 std::fill(std::begin(this->u
.industry
.other_accepted
), std::end(this->u
.industry
.other_accepted
), INVALID_CARGO
);
2051 std::fill(std::begin(this->u
.industry
.other_produced
), std::end(this->u
.industry
.other_produced
), INVALID_CARGO
);
2055 * Connect a cargo from an industry to the #CFT_CARGO column.
2056 * @param cargo Cargo to connect.
2057 * @param producer Cargo is produced (if \c false, cargo is assumed to be accepted).
2058 * @return Horizontal connection index, or \c -1 if not accepted at all.
2060 int ConnectCargo(CargoID cargo
, bool producer
)
2062 assert(this->type
== CFT_CARGO
);
2063 if (!IsValidCargoID(cargo
)) return -1;
2065 /* Find the vertical cargo column carrying the cargo. */
2067 for (int i
= 0; i
< this->u
.cargo
.num_cargoes
; i
++) {
2068 if (cargo
== this->u
.cargo
.vertical_cargoes
[i
]) {
2073 if (column
< 0) return -1;
2076 assert(!HasBit(this->u
.cargo
.supp_cargoes
, column
));
2077 SetBit(this->u
.cargo
.supp_cargoes
, column
);
2079 assert(!HasBit(this->u
.cargo
.cust_cargoes
, column
));
2080 SetBit(this->u
.cargo
.cust_cargoes
, column
);
2086 * Does this #CFT_CARGO field have a horizontal connection?
2087 * @return \c true if a horizontal connection exists, \c false otherwise.
2089 bool HasConnection()
2091 assert(this->type
== CFT_CARGO
);
2093 return this->u
.cargo
.supp_cargoes
!= 0 || this->u
.cargo
.cust_cargoes
!= 0;
2097 * Make a piece of cargo column.
2098 * @param cargoes Span of #CargoID (may contain #INVALID_CARGO).
2099 * @note #supp_cargoes and #cust_cargoes should be filled in later.
2101 void MakeCargo(const std::span
<const CargoID
> cargoes
)
2103 this->type
= CFT_CARGO
;
2104 assert(std::size(cargoes
) <= std::size(this->u
.cargo
.vertical_cargoes
));
2105 auto insert
= std::copy_if(std::begin(cargoes
), std::end(cargoes
), std::begin(this->u
.cargo
.vertical_cargoes
), IsValidCargoID
);
2106 this->u
.cargo
.num_cargoes
= static_cast<uint8_t>(std::distance(std::begin(this->u
.cargo
.vertical_cargoes
), insert
));
2107 CargoIDComparator comparator
;
2108 std::sort(std::begin(this->u
.cargo
.vertical_cargoes
), insert
, comparator
);
2109 std::fill(insert
, std::end(this->u
.cargo
.vertical_cargoes
), INVALID_CARGO
);
2110 this->u
.cargo
.top_end
= false;
2111 this->u
.cargo
.bottom_end
= false;
2112 this->u
.cargo
.supp_cargoes
= 0;
2113 this->u
.cargo
.cust_cargoes
= 0;
2117 * Make a field displaying cargo type names.
2118 * @param cargoes Span of #CargoID (may contain #INVALID_CARGO).
2119 * @param left_align ALign texts to the left (else to the right).
2121 void MakeCargoLabel(const std::span
<const CargoID
> cargoes
, bool left_align
)
2123 this->type
= CFT_CARGO_LABEL
;
2124 assert(std::size(cargoes
) <= std::size(this->u
.cargo_label
.cargoes
));
2125 auto insert
= std::copy(std::begin(cargoes
), std::end(cargoes
), std::begin(this->u
.cargo_label
.cargoes
));
2126 std::fill(insert
, std::end(this->u
.cargo_label
.cargoes
), INVALID_CARGO
);
2127 this->u
.cargo_label
.left_align
= left_align
;
2131 * Make a header above an industry column.
2132 * @param textid Text to display.
2134 void MakeHeader(StringID textid
)
2136 this->type
= CFT_HEADER
;
2137 this->u
.header
= textid
;
2141 * For a #CFT_CARGO, compute the left position of the left-most vertical cargo connection.
2142 * @param xpos Left position of the field.
2143 * @return Left position of the left-most vertical cargo column.
2145 int GetCargoBase(int xpos
) const
2147 assert(this->type
== CFT_CARGO
);
2148 int n
= this->u
.cargo
.num_cargoes
;
2150 return xpos
+ cargo_field_width
/ 2 - (CargoesField::cargo_line
.width
* n
+ CargoesField::cargo_space
.width
* (n
- 1)) / 2;
2155 * @param xpos Position of the left edge.
2156 * @param ypos Position of the top edge.
2158 void Draw(int xpos
, int ypos
) const
2160 switch (this->type
) {
2162 case CFT_SMALL_EMPTY
:
2166 ypos
+= (small_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
2167 DrawString(xpos
, xpos
+ industry_width
, ypos
, this->u
.header
, TC_WHITE
, SA_HOR_CENTER
);
2170 case CFT_INDUSTRY
: {
2171 int ypos1
= ypos
+ vert_inter_industry_space
/ 2;
2172 int ypos2
= ypos
+ normal_height
- 1 - vert_inter_industry_space
/ 2;
2173 int xpos2
= xpos
+ industry_width
- 1;
2174 DrawRectOutline({xpos
, ypos1
, xpos2
, ypos2
}, INDUSTRY_LINE_COLOUR
);
2175 ypos
+= (normal_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
2176 if (this->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2177 const IndustrySpec
*indsp
= GetIndustrySpec(this->u
.industry
.ind_type
);
2178 DrawString(xpos
, xpos2
, ypos
, indsp
->name
, TC_WHITE
, SA_HOR_CENTER
);
2180 /* Draw the industry legend. */
2181 int blob_left
, blob_right
;
2182 if (_current_text_dir
== TD_RTL
) {
2183 blob_right
= xpos2
- blob_distance
;
2184 blob_left
= blob_right
- CargoesField::legend
.width
;
2186 blob_left
= xpos
+ blob_distance
;
2187 blob_right
= blob_left
+ CargoesField::legend
.width
;
2189 GfxFillRect(blob_left
, ypos2
- blob_distance
- CargoesField::legend
.height
, blob_right
, ypos2
- blob_distance
, PC_BLACK
); // Border
2190 GfxFillRect(blob_left
+ 1, ypos2
- blob_distance
- CargoesField::legend
.height
+ 1, blob_right
- 1, ypos2
- blob_distance
- 1, indsp
->map_colour
);
2192 DrawString(xpos
, xpos2
, ypos
, STR_INDUSTRY_CARGOES_HOUSES
, TC_FROMSTRING
, SA_HOR_CENTER
);
2195 /* Draw the other_produced/other_accepted cargoes. */
2196 std::span
<const CargoID
> other_right
, other_left
;
2197 if (_current_text_dir
== TD_RTL
) {
2198 other_right
= this->u
.industry
.other_accepted
;
2199 other_left
= this->u
.industry
.other_produced
;
2201 other_right
= this->u
.industry
.other_produced
;
2202 other_left
= this->u
.industry
.other_accepted
;
2204 ypos1
+= CargoesField::cargo_border
.height
+ (GetCharacterHeight(FS_NORMAL
) - CargoesField::cargo_line
.height
) / 2;
2205 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2206 if (IsValidCargoID(other_right
[i
])) {
2207 const CargoSpec
*csp
= CargoSpec::Get(other_right
[i
]);
2208 int xp
= xpos
+ industry_width
+ CargoesField::cargo_stub
.width
;
2209 DrawHorConnection(xpos
+ industry_width
, xp
- 1, ypos1
, csp
);
2210 GfxDrawLine(xp
, ypos1
, xp
, ypos1
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2212 if (IsValidCargoID(other_left
[i
])) {
2213 const CargoSpec
*csp
= CargoSpec::Get(other_left
[i
]);
2214 int xp
= xpos
- CargoesField::cargo_stub
.width
;
2215 DrawHorConnection(xp
+ 1, xpos
- 1, ypos1
, csp
);
2216 GfxDrawLine(xp
, ypos1
, xp
, ypos1
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2218 ypos1
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2224 int cargo_base
= this->GetCargoBase(xpos
);
2225 int top
= ypos
+ (this->u
.cargo
.top_end
? vert_inter_industry_space
/ 2 + 1 : 0);
2226 int bot
= ypos
- (this->u
.cargo
.bottom_end
? vert_inter_industry_space
/ 2 + 1 : 0) + normal_height
- 1;
2227 int colpos
= cargo_base
;
2228 for (int i
= 0; i
< this->u
.cargo
.num_cargoes
; i
++) {
2229 if (this->u
.cargo
.top_end
) GfxDrawLine(colpos
, top
- 1, colpos
+ CargoesField::cargo_line
.width
- 1, top
- 1, CARGO_LINE_COLOUR
);
2230 if (this->u
.cargo
.bottom_end
) GfxDrawLine(colpos
, bot
+ 1, colpos
+ CargoesField::cargo_line
.width
- 1, bot
+ 1, CARGO_LINE_COLOUR
);
2231 GfxDrawLine(colpos
, top
, colpos
, bot
, CARGO_LINE_COLOUR
);
2233 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[i
]);
2234 GfxFillRect(colpos
, top
, colpos
+ CargoesField::cargo_line
.width
- 2, bot
, csp
->legend_colour
, FILLRECT_OPAQUE
);
2235 colpos
+= CargoesField::cargo_line
.width
- 2;
2236 GfxDrawLine(colpos
, top
, colpos
, bot
, CARGO_LINE_COLOUR
);
2237 colpos
+= 1 + CargoesField::cargo_space
.width
;
2240 Cargoes hor_left
, hor_right
;
2241 if (_current_text_dir
== TD_RTL
) {
2242 hor_left
= this->u
.cargo
.cust_cargoes
;
2243 hor_right
= this->u
.cargo
.supp_cargoes
;
2245 hor_left
= this->u
.cargo
.supp_cargoes
;
2246 hor_right
= this->u
.cargo
.cust_cargoes
;
2248 ypos
+= CargoesField::cargo_border
.height
+ vert_inter_industry_space
/ 2 + (GetCharacterHeight(FS_NORMAL
) - CargoesField::cargo_line
.height
) / 2;
2249 for (uint i
= 0; i
< MAX_CARGOES
; i
++) {
2250 if (HasBit(hor_left
, i
)) {
2253 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[col
]);
2254 for (; col
> 0; col
--) {
2255 int lf
= cargo_base
+ col
* CargoesField::cargo_line
.width
+ (col
- 1) * CargoesField::cargo_space
.width
;
2256 DrawHorConnection(lf
, lf
+ CargoesField::cargo_space
.width
- dx
, ypos
, csp
);
2259 DrawHorConnection(xpos
, cargo_base
- dx
, ypos
, csp
);
2261 if (HasBit(hor_right
, i
)) {
2264 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[col
]);
2265 for (; col
< this->u
.cargo
.num_cargoes
- 1; col
++) {
2266 int lf
= cargo_base
+ (col
+ 1) * CargoesField::cargo_line
.width
+ col
* CargoesField::cargo_space
.width
;
2267 DrawHorConnection(lf
+ dx
- 1, lf
+ CargoesField::cargo_space
.width
- 1, ypos
, csp
);
2270 DrawHorConnection(cargo_base
+ col
* CargoesField::cargo_space
.width
+ (col
+ 1) * CargoesField::cargo_line
.width
- 1 + dx
, xpos
+ CargoesField::cargo_field_width
- 1, ypos
, csp
);
2272 ypos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2277 case CFT_CARGO_LABEL
:
2278 ypos
+= CargoesField::cargo_border
.height
+ vert_inter_industry_space
/ 2;
2279 for (uint i
= 0; i
< MAX_CARGOES
; i
++) {
2280 if (IsValidCargoID(this->u
.cargo_label
.cargoes
[i
])) {
2281 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo_label
.cargoes
[i
]);
2282 DrawString(xpos
+ WidgetDimensions::scaled
.framerect
.left
, xpos
+ industry_width
- 1 - WidgetDimensions::scaled
.framerect
.right
, ypos
, csp
->name
, TC_WHITE
,
2283 (this->u
.cargo_label
.left_align
) ? SA_LEFT
: SA_RIGHT
);
2285 ypos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2295 * Decide which cargo was clicked at in a #CFT_CARGO field.
2296 * @param left Left industry neighbour if available (else \c nullptr should be supplied).
2297 * @param right Right industry neighbour if available (else \c nullptr should be supplied).
2298 * @param pt Click position in the cargo field.
2299 * @return Cargo clicked at, or #INVALID_CARGO if none.
2301 CargoID
CargoClickedAt(const CargoesField
*left
, const CargoesField
*right
, Point pt
) const
2303 assert(this->type
== CFT_CARGO
);
2305 /* Vertical matching. */
2306 int cpos
= this->GetCargoBase(0);
2308 for (col
= 0; col
< this->u
.cargo
.num_cargoes
; col
++) {
2309 if (pt
.x
< cpos
) break;
2310 if (pt
.x
< cpos
+ (int)CargoesField::cargo_line
.width
) return this->u
.cargo
.vertical_cargoes
[col
];
2311 cpos
+= CargoesField::cargo_line
.width
+ CargoesField::cargo_space
.width
;
2313 /* col = 0 -> left of first col, 1 -> left of 2nd col, ... this->u.cargo.num_cargoes right of last-col. */
2315 int vpos
= vert_inter_industry_space
/ 2 + CargoesField::cargo_border
.width
;
2317 for (row
= 0; row
< MAX_CARGOES
; row
++) {
2318 if (pt
.y
< vpos
) return INVALID_CARGO
;
2319 if (pt
.y
< vpos
+ GetCharacterHeight(FS_NORMAL
)) break;
2320 vpos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.width
;
2322 if (row
== MAX_CARGOES
) return INVALID_CARGO
;
2324 /* row = 0 -> at first horizontal row, row = 1 -> second horizontal row, 2 = 3rd horizontal row. */
2326 if (HasBit(this->u
.cargo
.supp_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2327 if (left
!= nullptr) {
2328 if (left
->type
== CFT_INDUSTRY
) return left
->u
.industry
.other_produced
[row
];
2329 if (left
->type
== CFT_CARGO_LABEL
&& !left
->u
.cargo_label
.left_align
) return left
->u
.cargo_label
.cargoes
[row
];
2331 return INVALID_CARGO
;
2333 if (col
== this->u
.cargo
.num_cargoes
) {
2334 if (HasBit(this->u
.cargo
.cust_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2335 if (right
!= nullptr) {
2336 if (right
->type
== CFT_INDUSTRY
) return right
->u
.industry
.other_accepted
[row
];
2337 if (right
->type
== CFT_CARGO_LABEL
&& right
->u
.cargo_label
.left_align
) return right
->u
.cargo_label
.cargoes
[row
];
2339 return INVALID_CARGO
;
2342 /* Clicked somewhere in-between vertical cargo connection.
2343 * Since the horizontal connection is made in the same order as the vertical list, the above condition
2344 * ensures we are left-below the main diagonal, thus at the supplying side.
2346 if (HasBit(this->u
.cargo
.supp_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2347 return INVALID_CARGO
;
2349 /* Clicked at a customer connection. */
2350 if (HasBit(this->u
.cargo
.cust_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2351 return INVALID_CARGO
;
2355 * Decide what cargo the user clicked in the cargo label field.
2356 * @param pt Click position in the cargo label field.
2357 * @return Cargo clicked at, or #INVALID_CARGO if none.
2359 CargoID
CargoLabelClickedAt(Point pt
) const
2361 assert(this->type
== CFT_CARGO_LABEL
);
2363 int vpos
= vert_inter_industry_space
/ 2 + CargoesField::cargo_border
.height
;
2365 for (row
= 0; row
< MAX_CARGOES
; row
++) {
2366 if (pt
.y
< vpos
) return INVALID_CARGO
;
2367 if (pt
.y
< vpos
+ GetCharacterHeight(FS_NORMAL
)) break;
2368 vpos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2370 if (row
== MAX_CARGOES
) return INVALID_CARGO
;
2371 return this->u
.cargo_label
.cargoes
[row
];
2376 * Draw a horizontal cargo connection.
2377 * @param left Left-most coordinate to draw.
2378 * @param right Right-most coordinate to draw.
2379 * @param top Top coordinate of the cargo connection.
2380 * @param csp Cargo to draw.
2382 static void DrawHorConnection(int left
, int right
, int top
, const CargoSpec
*csp
)
2384 GfxDrawLine(left
, top
, right
, top
, CARGO_LINE_COLOUR
);
2385 GfxFillRect(left
, top
+ 1, right
, top
+ CargoesField::cargo_line
.height
- 2, csp
->legend_colour
, FILLRECT_OPAQUE
);
2386 GfxDrawLine(left
, top
+ CargoesField::cargo_line
.height
- 1, right
, top
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2390 static_assert(MAX_CARGOES
>= std::tuple_size_v
<decltype(IndustrySpec::produced_cargo
)>);
2391 static_assert(MAX_CARGOES
>= std::tuple_size_v
<decltype(IndustrySpec::accepts_cargo
)>);
2393 Dimension
CargoesField::legend
; ///< Dimension of the legend blob.
2394 Dimension
CargoesField::cargo_border
; ///< Dimensions of border between cargo lines and industry boxes.
2395 Dimension
CargoesField::cargo_line
; ///< Dimensions of cargo lines.
2396 Dimension
CargoesField::cargo_space
; ///< Dimensions of space between cargo lines.
2397 Dimension
CargoesField::cargo_stub
; ///< Dimensions of cargo stub (unconnected cargo line.)
2399 int CargoesField::small_height
; ///< Height of the header row.
2400 int CargoesField::normal_height
; ///< Height of the non-header rows.
2401 int CargoesField::industry_width
; ///< Width of an industry field.
2402 int CargoesField::cargo_field_width
; ///< Width of a cargo field.
2403 uint
CargoesField::max_cargoes
; ///< Largest number of cargoes actually on any industry.
2404 int CargoesField::vert_inter_industry_space
; ///< Amount of space between two industries in a column.
2406 int CargoesField::blob_distance
; ///< Distance of the industry legend colour from the edge of the industry box.
2408 const int CargoesField::INDUSTRY_LINE_COLOUR
= PC_YELLOW
; ///< Line colour of the industry type box.
2409 const int CargoesField::CARGO_LINE_COLOUR
= PC_YELLOW
; ///< Line colour around the cargo.
2411 /** A single row of #CargoesField. */
2413 CargoesField columns
[5]; ///< One row of fields.
2416 * Connect industry production cargoes to the cargo column after it.
2417 * @param column Column of the industry.
2419 void ConnectIndustryProduced(int column
)
2421 CargoesField
*ind_fld
= this->columns
+ column
;
2422 CargoesField
*cargo_fld
= this->columns
+ column
+ 1;
2423 assert(ind_fld
->type
== CFT_INDUSTRY
&& cargo_fld
->type
== CFT_CARGO
);
2425 std::fill(std::begin(ind_fld
->u
.industry
.other_produced
), std::end(ind_fld
->u
.industry
.other_produced
), INVALID_CARGO
);
2427 if (ind_fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2428 CargoID others
[MAX_CARGOES
]; // Produced cargoes not carried in the cargo column.
2429 int other_count
= 0;
2431 const IndustrySpec
*indsp
= GetIndustrySpec(ind_fld
->u
.industry
.ind_type
);
2432 assert(CargoesField::max_cargoes
<= std::size(indsp
->produced_cargo
));
2433 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2434 int col
= cargo_fld
->ConnectCargo(indsp
->produced_cargo
[i
], true);
2435 if (col
< 0) others
[other_count
++] = indsp
->produced_cargo
[i
];
2438 /* Allocate other cargoes in the empty holes of the horizontal cargo connections. */
2439 for (uint i
= 0; i
< CargoesField::max_cargoes
&& other_count
> 0; i
++) {
2440 if (HasBit(cargo_fld
->u
.cargo
.supp_cargoes
, i
)) ind_fld
->u
.industry
.other_produced
[i
] = others
[--other_count
];
2443 /* Houses only display cargo that towns produce. */
2444 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2445 CargoID cid
= cargo_fld
->u
.cargo
.vertical_cargoes
[i
];
2446 TownProductionEffect tpe
= CargoSpec::Get(cid
)->town_production_effect
;
2447 if (tpe
== TPE_PASSENGERS
|| tpe
== TPE_MAIL
) cargo_fld
->ConnectCargo(cid
, true);
2453 * Construct a #CFT_CARGO_LABEL field.
2454 * @param column Column to create the new field.
2455 * @param accepting Display accepted cargo (if \c false, display produced cargo).
2457 void MakeCargoLabel(int column
, bool accepting
)
2459 CargoID cargoes
[MAX_CARGOES
];
2460 std::fill(std::begin(cargoes
), std::end(cargoes
), INVALID_CARGO
);
2462 CargoesField
*label_fld
= this->columns
+ column
;
2463 CargoesField
*cargo_fld
= this->columns
+ (accepting
? column
- 1 : column
+ 1);
2465 assert(cargo_fld
->type
== CFT_CARGO
&& label_fld
->type
== CFT_EMPTY
);
2466 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2467 int col
= cargo_fld
->ConnectCargo(cargo_fld
->u
.cargo
.vertical_cargoes
[i
], !accepting
);
2468 if (col
>= 0) cargoes
[col
] = cargo_fld
->u
.cargo
.vertical_cargoes
[i
];
2470 label_fld
->MakeCargoLabel(cargoes
, accepting
);
2475 * Connect industry accepted cargoes to the cargo column before it.
2476 * @param column Column of the industry.
2478 void ConnectIndustryAccepted(int column
)
2480 CargoesField
*ind_fld
= this->columns
+ column
;
2481 CargoesField
*cargo_fld
= this->columns
+ column
- 1;
2482 assert(ind_fld
->type
== CFT_INDUSTRY
&& cargo_fld
->type
== CFT_CARGO
);
2484 std::fill(std::begin(ind_fld
->u
.industry
.other_accepted
), std::end(ind_fld
->u
.industry
.other_accepted
), INVALID_CARGO
);
2486 if (ind_fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2487 CargoID others
[MAX_CARGOES
]; // Accepted cargoes not carried in the cargo column.
2488 int other_count
= 0;
2490 const IndustrySpec
*indsp
= GetIndustrySpec(ind_fld
->u
.industry
.ind_type
);
2491 assert(CargoesField::max_cargoes
<= std::size(indsp
->accepts_cargo
));
2492 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2493 int col
= cargo_fld
->ConnectCargo(indsp
->accepts_cargo
[i
], false);
2494 if (col
< 0) others
[other_count
++] = indsp
->accepts_cargo
[i
];
2497 /* Allocate other cargoes in the empty holes of the horizontal cargo connections. */
2498 for (uint i
= 0; i
< CargoesField::max_cargoes
&& other_count
> 0; i
++) {
2499 if (!HasBit(cargo_fld
->u
.cargo
.cust_cargoes
, i
)) ind_fld
->u
.industry
.other_accepted
[i
] = others
[--other_count
];
2502 /* Houses only display what is demanded. */
2503 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2504 for (const auto &hs
: HouseSpec::Specs()) {
2505 if (!hs
.enabled
) continue;
2507 for (uint j
= 0; j
< lengthof(hs
.accepts_cargo
); j
++) {
2508 if (hs
.cargo_acceptance
[j
] > 0 && cargo_fld
->u
.cargo
.vertical_cargoes
[i
] == hs
.accepts_cargo
[j
]) {
2509 cargo_fld
->ConnectCargo(cargo_fld
->u
.cargo
.vertical_cargoes
[i
], false);
2522 * Window displaying the cargo connections around an industry (or cargo).
2524 * The main display is constructed from 'fields', rectangles that contain an industry, piece of the cargo connection, cargo labels, or headers.
2525 * For a nice display, the following should be kept in mind:
2526 * - A #CFT_HEADER is always at the top of an column of #CFT_INDUSTRY fields.
2527 * - A #CFT_CARGO_LABEL field is also always put in a column of #CFT_INDUSTRY fields.
2528 * - The top row contains #CFT_HEADER and #CFT_SMALL_EMPTY fields.
2529 * - Cargo connections have a column of their own (#CFT_CARGO fields).
2530 * - Cargo accepted or produced by an industry, but not carried in a cargo connection, is drawn in the space of a cargo column attached to the industry.
2531 * The information however is part of the industry.
2533 * This results in the following invariants:
2534 * - Width of a #CFT_INDUSTRY column is large enough to hold all industry type labels, all cargo labels, and all header texts.
2535 * - Height of a #CFT_INDUSTRY is large enough to hold a header line, or a industry type line, \c N cargo labels
2536 * (where \c N is the maximum number of cargoes connected between industries), \c N connections of cargo types, and space
2537 * between two industry types (1/2 above it, and 1/2 underneath it).
2538 * - Width of a cargo field (#CFT_CARGO) is large enough to hold \c N vertical columns (one for each type of cargo).
2539 * Also, space is needed between an industry and the leftmost/rightmost column to draw the non-carried cargoes.
2540 * - Height of a #CFT_CARGO field is equally high as the height of the #CFT_INDUSTRY.
2541 * - A field at the top (#CFT_HEADER or #CFT_SMALL_EMPTY) match the width of the fields below them (#CFT_INDUSTRY respectively
2542 * #CFT_CARGO), the height should be sufficient to display the header text.
2544 * When displaying the cargoes around an industry type, five columns are needed (supplying industries, accepted cargoes, the industry,
2545 * produced cargoes, customer industries). Displaying the industries around a cargo needs three columns (supplying industries, the cargo,
2546 * customer industries). The remaining two columns are set to #CFT_EMPTY with a width equal to the average of a cargo and an industry column.
2548 struct IndustryCargoesWindow
: public Window
{
2549 typedef std::vector
<CargoesRow
> Fields
;
2551 Fields fields
; ///< Fields to display in the #WID_IC_PANEL.
2552 uint ind_cargo
; ///< If less than #NUM_INDUSTRYTYPES, an industry type, else a cargo id + NUM_INDUSTRYTYPES.
2553 Dimension cargo_textsize
; ///< Size to hold any cargo text, as well as STR_INDUSTRY_CARGOES_SELECT_CARGO.
2554 Dimension ind_textsize
; ///< Size to hold any industry type text, as well as STR_INDUSTRY_CARGOES_SELECT_INDUSTRY.
2557 IndustryCargoesWindow(int id
) : Window(_industry_cargoes_desc
)
2560 this->CreateNestedTree();
2561 this->vscroll
= this->GetScrollbar(WID_IC_SCROLLBAR
);
2562 this->FinishInitNested(0);
2563 this->OnInvalidateData(id
);
2566 void OnInit() override
2568 /* Initialize static CargoesField size variables. */
2569 Dimension d
= GetStringBoundingBox(STR_INDUSTRY_CARGOES_PRODUCERS
);
2570 d
= maxdim(d
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_CUSTOMERS
));
2571 d
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
2572 d
.height
+= WidgetDimensions::scaled
.frametext
.Vertical();
2573 CargoesField::small_height
= d
.height
;
2575 /* Size of the legend blob -- slightly larger than the smallmap legend blob. */
2576 CargoesField::legend
.height
= GetCharacterHeight(FS_SMALL
);
2577 CargoesField::legend
.width
= CargoesField::legend
.height
* 9 / 6;
2579 /* Size of cargo lines. */
2580 CargoesField::cargo_line
.width
= ScaleGUITrad(6);
2581 CargoesField::cargo_line
.height
= CargoesField::cargo_line
.width
;
2583 /* Size of border between cargo lines and industry boxes. */
2584 CargoesField::cargo_border
.width
= CargoesField::cargo_line
.width
* 3 / 2;
2585 CargoesField::cargo_border
.height
= CargoesField::cargo_line
.width
/ 2;
2587 /* Size of space between cargo lines. */
2588 CargoesField::cargo_space
.width
= CargoesField::cargo_line
.width
/ 2;
2589 CargoesField::cargo_space
.height
= CargoesField::cargo_line
.height
/ 2;
2591 /* Size of cargo stub (unconnected cargo line.) */
2592 CargoesField::cargo_stub
.width
= CargoesField::cargo_line
.width
/ 2;
2593 CargoesField::cargo_stub
.height
= CargoesField::cargo_line
.height
; /* Unused */
2595 CargoesField::vert_inter_industry_space
= WidgetDimensions::scaled
.vsep_wide
;
2596 CargoesField::blob_distance
= WidgetDimensions::scaled
.hsep_normal
;
2598 /* Decide about the size of the box holding the text of an industry type. */
2599 this->ind_textsize
.width
= 0;
2600 this->ind_textsize
.height
= 0;
2601 CargoesField::max_cargoes
= 0;
2602 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2603 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2604 if (!indsp
->enabled
) continue;
2605 this->ind_textsize
= maxdim(this->ind_textsize
, GetStringBoundingBox(indsp
->name
));
2606 CargoesField::max_cargoes
= std::max
<uint
>(CargoesField::max_cargoes
, std::count_if(std::begin(indsp
->accepts_cargo
), std::end(indsp
->accepts_cargo
), IsValidCargoID
));
2607 CargoesField::max_cargoes
= std::max
<uint
>(CargoesField::max_cargoes
, std::count_if(std::begin(indsp
->produced_cargo
), std::end(indsp
->produced_cargo
), IsValidCargoID
));
2609 d
.width
= std::max(d
.width
, this->ind_textsize
.width
);
2610 d
.height
= this->ind_textsize
.height
;
2611 this->ind_textsize
= maxdim(this->ind_textsize
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_SELECT_INDUSTRY
));
2613 /* Compute max size of the cargo texts. */
2614 this->cargo_textsize
.width
= 0;
2615 this->cargo_textsize
.height
= 0;
2616 for (const CargoSpec
*csp
: CargoSpec::Iterate()) {
2617 if (!csp
->IsValid()) continue;
2618 this->cargo_textsize
= maxdim(this->cargo_textsize
, GetStringBoundingBox(csp
->name
));
2620 d
= maxdim(d
, this->cargo_textsize
); // Box must also be wide enough to hold any cargo label.
2621 this->cargo_textsize
= maxdim(this->cargo_textsize
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_SELECT_CARGO
));
2623 d
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
2624 /* Ensure the height is enough for the industry type text, for the horizontal connections, and for the cargo labels. */
2625 uint min_ind_height
= CargoesField::cargo_border
.height
* 2 + CargoesField::max_cargoes
* GetCharacterHeight(FS_NORMAL
) + (CargoesField::max_cargoes
- 1) * CargoesField::cargo_space
.height
;
2626 d
.height
= std::max(d
.height
+ WidgetDimensions::scaled
.frametext
.Vertical(), min_ind_height
);
2628 CargoesField::industry_width
= d
.width
;
2629 CargoesField::normal_height
= d
.height
+ CargoesField::vert_inter_industry_space
;
2631 /* Width of a #CFT_CARGO field. */
2632 CargoesField::cargo_field_width
= CargoesField::cargo_border
.width
* 2 + CargoesField::cargo_line
.width
* CargoesField::max_cargoes
+ CargoesField::cargo_space
.width
* (CargoesField::max_cargoes
- 1);
2635 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
2639 resize
.height
= CargoesField::normal_height
;
2640 size
.width
= CargoesField::industry_width
* 3 + CargoesField::cargo_field_width
* 2 + WidgetDimensions::scaled
.frametext
.Horizontal();
2641 size
.height
= CargoesField::small_height
+ 2 * resize
.height
+ WidgetDimensions::scaled
.frametext
.Vertical();
2644 case WID_IC_IND_DROPDOWN
:
2645 size
.width
= std::max(size
.width
, this->ind_textsize
.width
+ padding
.width
);
2648 case WID_IC_CARGO_DROPDOWN
:
2649 size
.width
= std::max(size
.width
, this->cargo_textsize
.width
+ padding
.width
);
2654 void SetStringParameters(WidgetID widget
) const override
2656 if (widget
!= WID_IC_CAPTION
) return;
2658 if (this->ind_cargo
< NUM_INDUSTRYTYPES
) {
2659 const IndustrySpec
*indsp
= GetIndustrySpec(this->ind_cargo
);
2660 SetDParam(0, indsp
->name
);
2662 const CargoSpec
*csp
= CargoSpec::Get(this->ind_cargo
- NUM_INDUSTRYTYPES
);
2663 SetDParam(0, csp
->name
);
2668 * Do the two sets of cargoes have a valid cargo in common?
2669 * @param cargoes1 Span of the first cargo list.
2670 * @param cargoes2 Span of the second cargo list.
2671 * @return Arrays have at least one valid cargo in common.
2673 static bool HasCommonValidCargo(const std::span
<const CargoID
> cargoes1
, const std::span
<const CargoID
> cargoes2
)
2675 for (const CargoID cid1
: cargoes1
) {
2676 if (!IsValidCargoID(cid1
)) continue;
2677 for (const CargoID cid2
: cargoes2
) {
2678 if (cid1
== cid2
) return true;
2685 * Can houses be used to supply one of the cargoes?
2686 * @param cargoes Span of cargo list.
2687 * @return Houses can supply at least one of the cargoes.
2689 static bool HousesCanSupply(const std::span
<const CargoID
> cargoes
)
2691 for (const CargoID cid
: cargoes
) {
2692 if (!IsValidCargoID(cid
)) continue;
2693 TownProductionEffect tpe
= CargoSpec::Get(cid
)->town_production_effect
;
2694 if (tpe
== TPE_PASSENGERS
|| tpe
== TPE_MAIL
) return true;
2700 * Can houses be used as customers of the produced cargoes?
2701 * @param cargoes Span of cargo list.
2702 * @return Houses can accept at least one of the cargoes.
2704 static bool HousesCanAccept(const std::span
<const CargoID
> cargoes
)
2706 HouseZones climate_mask
;
2707 switch (_settings_game
.game_creation
.landscape
) {
2708 case LT_TEMPERATE
: climate_mask
= HZ_TEMP
; break;
2709 case LT_ARCTIC
: climate_mask
= HZ_SUBARTC_ABOVE
| HZ_SUBARTC_BELOW
; break;
2710 case LT_TROPIC
: climate_mask
= HZ_SUBTROPIC
; break;
2711 case LT_TOYLAND
: climate_mask
= HZ_TOYLND
; break;
2712 default: NOT_REACHED();
2714 for (const CargoID cid
: cargoes
) {
2715 if (!IsValidCargoID(cid
)) continue;
2717 for (const auto &hs
: HouseSpec::Specs()) {
2718 if (!hs
.enabled
|| !(hs
.building_availability
& climate_mask
)) continue;
2720 for (uint j
= 0; j
< lengthof(hs
.accepts_cargo
); j
++) {
2721 if (hs
.cargo_acceptance
[j
] > 0 && cid
== hs
.accepts_cargo
[j
]) return true;
2729 * Count how many industries have accepted cargoes in common with one of the supplied set.
2730 * @param cargoes Cargoes to search.
2731 * @return Number of industries that have an accepted cargo in common with the supplied set.
2733 static int CountMatchingAcceptingIndustries(const std::span
<const CargoID
> cargoes
)
2736 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2737 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2738 if (!indsp
->enabled
) continue;
2740 if (HasCommonValidCargo(cargoes
, indsp
->accepts_cargo
)) count
++;
2746 * Count how many industries have produced cargoes in common with one of the supplied set.
2747 * @param cargoes Cargoes to search.
2748 * @return Number of industries that have a produced cargo in common with the supplied set.
2750 static int CountMatchingProducingIndustries(const std::span
<const CargoID
> cargoes
)
2753 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2754 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2755 if (!indsp
->enabled
) continue;
2757 if (HasCommonValidCargo(cargoes
, indsp
->produced_cargo
)) count
++;
2763 * Shorten the cargo column to just the part between industries.
2764 * @param column Column number of the cargo column.
2765 * @param top Current top row.
2766 * @param bottom Current bottom row.
2768 void ShortenCargoColumn(int column
, int top
, int bottom
)
2770 while (top
< bottom
&& !this->fields
[top
].columns
[column
].HasConnection()) {
2771 this->fields
[top
].columns
[column
].MakeEmpty(CFT_EMPTY
);
2774 this->fields
[top
].columns
[column
].u
.cargo
.top_end
= true;
2776 while (bottom
> top
&& !this->fields
[bottom
].columns
[column
].HasConnection()) {
2777 this->fields
[bottom
].columns
[column
].MakeEmpty(CFT_EMPTY
);
2780 this->fields
[bottom
].columns
[column
].u
.cargo
.bottom_end
= true;
2784 * Place an industry in the fields.
2785 * @param row Row of the new industry.
2786 * @param col Column of the new industry.
2787 * @param it Industry to place.
2789 void PlaceIndustry(int row
, int col
, IndustryType it
)
2791 assert(this->fields
[row
].columns
[col
].type
== CFT_EMPTY
);
2792 this->fields
[row
].columns
[col
].MakeIndustry(it
);
2794 this->fields
[row
].ConnectIndustryProduced(col
);
2796 this->fields
[row
].ConnectIndustryAccepted(col
);
2801 * Notify smallmap that new displayed industries have been selected (in #_displayed_industries).
2803 void NotifySmallmap()
2805 if (!this->IsWidgetLowered(WID_IC_NOTIFY
)) return;
2807 /* Only notify the smallmap window if it exists. In particular, do not
2808 * bring it to the front to prevent messing up any nice layout of the user. */
2809 InvalidateWindowClassesData(WC_SMALLMAP
, 0);
2813 * Compute what and where to display for industry type \a it.
2814 * @param displayed_it Industry type to display.
2816 void ComputeIndustryDisplay(IndustryType displayed_it
)
2818 this->GetWidget
<NWidgetCore
>(WID_IC_CAPTION
)->widget_data
= STR_INDUSTRY_CARGOES_INDUSTRY_CAPTION
;
2819 this->ind_cargo
= displayed_it
;
2820 _displayed_industries
.reset();
2821 _displayed_industries
.set(displayed_it
);
2823 this->fields
.clear();
2824 CargoesRow
&first_row
= this->fields
.emplace_back();
2825 first_row
.columns
[0].MakeHeader(STR_INDUSTRY_CARGOES_PRODUCERS
);
2826 first_row
.columns
[1].MakeEmpty(CFT_SMALL_EMPTY
);
2827 first_row
.columns
[2].MakeEmpty(CFT_SMALL_EMPTY
);
2828 first_row
.columns
[3].MakeEmpty(CFT_SMALL_EMPTY
);
2829 first_row
.columns
[4].MakeHeader(STR_INDUSTRY_CARGOES_CUSTOMERS
);
2831 const IndustrySpec
*central_sp
= GetIndustrySpec(displayed_it
);
2832 bool houses_supply
= HousesCanSupply(central_sp
->accepts_cargo
);
2833 bool houses_accept
= HousesCanAccept(central_sp
->produced_cargo
);
2834 /* Make a field consisting of two cargo columns. */
2835 int num_supp
= CountMatchingProducingIndustries(central_sp
->accepts_cargo
) + houses_supply
;
2836 int num_cust
= CountMatchingAcceptingIndustries(central_sp
->produced_cargo
) + houses_accept
;
2837 int num_indrows
= std::max(3, std::max(num_supp
, num_cust
)); // One is needed for the 'it' industry, and 2 for the cargo labels.
2838 for (int i
= 0; i
< num_indrows
; i
++) {
2839 CargoesRow
&row
= this->fields
.emplace_back();
2840 row
.columns
[0].MakeEmpty(CFT_EMPTY
);
2841 row
.columns
[1].MakeCargo(central_sp
->accepts_cargo
);
2842 row
.columns
[2].MakeEmpty(CFT_EMPTY
);
2843 row
.columns
[3].MakeCargo(central_sp
->produced_cargo
);
2844 row
.columns
[4].MakeEmpty(CFT_EMPTY
);
2846 /* Add central industry. */
2847 int central_row
= 1 + num_indrows
/ 2;
2848 this->fields
[central_row
].columns
[2].MakeIndustry(displayed_it
);
2849 this->fields
[central_row
].ConnectIndustryProduced(2);
2850 this->fields
[central_row
].ConnectIndustryAccepted(2);
2852 /* Add cargo labels. */
2853 this->fields
[central_row
- 1].MakeCargoLabel(2, true);
2854 this->fields
[central_row
+ 1].MakeCargoLabel(2, false);
2856 /* Add suppliers and customers of the 'it' industry. */
2859 for (IndustryType it
: _sorted_industry_types
) {
2860 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2861 if (!indsp
->enabled
) continue;
2863 if (HasCommonValidCargo(central_sp
->accepts_cargo
, indsp
->produced_cargo
)) {
2864 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, it
);
2865 _displayed_industries
.set(it
);
2868 if (HasCommonValidCargo(central_sp
->produced_cargo
, indsp
->accepts_cargo
)) {
2869 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 4, it
);
2870 _displayed_industries
.set(it
);
2874 if (houses_supply
) {
2875 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, NUM_INDUSTRYTYPES
);
2878 if (houses_accept
) {
2879 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 4, NUM_INDUSTRYTYPES
);
2883 this->ShortenCargoColumn(1, 1, num_indrows
);
2884 this->ShortenCargoColumn(3, 1, num_indrows
);
2885 this->vscroll
->SetCount(num_indrows
);
2887 this->NotifySmallmap();
2891 * Compute what and where to display for cargo id \a cid.
2892 * @param cid Cargo id to display.
2894 void ComputeCargoDisplay(CargoID cid
)
2896 this->GetWidget
<NWidgetCore
>(WID_IC_CAPTION
)->widget_data
= STR_INDUSTRY_CARGOES_CARGO_CAPTION
;
2897 this->ind_cargo
= cid
+ NUM_INDUSTRYTYPES
;
2898 _displayed_industries
.reset();
2900 this->fields
.clear();
2901 CargoesRow
&first_row
= this->fields
.emplace_back();
2902 first_row
.columns
[0].MakeHeader(STR_INDUSTRY_CARGOES_PRODUCERS
);
2903 first_row
.columns
[1].MakeEmpty(CFT_SMALL_EMPTY
);
2904 first_row
.columns
[2].MakeHeader(STR_INDUSTRY_CARGOES_CUSTOMERS
);
2905 first_row
.columns
[3].MakeEmpty(CFT_SMALL_EMPTY
);
2906 first_row
.columns
[4].MakeEmpty(CFT_SMALL_EMPTY
);
2908 auto cargoes
= std::span(&cid
, 1);
2909 bool houses_supply
= HousesCanSupply(cargoes
);
2910 bool houses_accept
= HousesCanAccept(cargoes
);
2911 int num_supp
= CountMatchingProducingIndustries(cargoes
) + houses_supply
+ 1; // Ensure room for the cargo label.
2912 int num_cust
= CountMatchingAcceptingIndustries(cargoes
) + houses_accept
;
2913 int num_indrows
= std::max(num_supp
, num_cust
);
2914 for (int i
= 0; i
< num_indrows
; i
++) {
2915 CargoesRow
&row
= this->fields
.emplace_back();
2916 row
.columns
[0].MakeEmpty(CFT_EMPTY
);
2917 row
.columns
[1].MakeCargo(cargoes
);
2918 row
.columns
[2].MakeEmpty(CFT_EMPTY
);
2919 row
.columns
[3].MakeEmpty(CFT_EMPTY
);
2920 row
.columns
[4].MakeEmpty(CFT_EMPTY
);
2923 this->fields
[num_indrows
].MakeCargoLabel(0, false); // Add cargo labels at the left bottom.
2925 /* Add suppliers and customers of the cargo. */
2928 for (IndustryType it
: _sorted_industry_types
) {
2929 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2930 if (!indsp
->enabled
) continue;
2932 if (HasCommonValidCargo(cargoes
, indsp
->produced_cargo
)) {
2933 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, it
);
2934 _displayed_industries
.set(it
);
2937 if (HasCommonValidCargo(cargoes
, indsp
->accepts_cargo
)) {
2938 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 2, it
);
2939 _displayed_industries
.set(it
);
2943 if (houses_supply
) {
2944 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, NUM_INDUSTRYTYPES
);
2947 if (houses_accept
) {
2948 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 2, NUM_INDUSTRYTYPES
);
2952 this->ShortenCargoColumn(1, 1, num_indrows
);
2953 this->vscroll
->SetCount(num_indrows
);
2955 this->NotifySmallmap();
2959 * Some data on this window has become invalid.
2960 * @param data Information about the changed data.
2961 * - data = 0 .. NUM_INDUSTRYTYPES - 1: Display the chain around the given industry.
2962 * - data = NUM_INDUSTRYTYPES: Stop sending updates to the smallmap window.
2963 * @param gui_scope Whether the call is done from GUI scope. You may not do everything when not in GUI scope. See #InvalidateWindowData() for details.
2965 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
2967 if (!gui_scope
) return;
2968 if (data
== NUM_INDUSTRYTYPES
) {
2969 this->RaiseWidgetWhenLowered(WID_IC_NOTIFY
);
2973 assert(data
>= 0 && data
< NUM_INDUSTRYTYPES
);
2974 this->ComputeIndustryDisplay(data
);
2977 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
2979 if (widget
!= WID_IC_PANEL
) return;
2981 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.bevel
);
2982 DrawPixelInfo tmp_dpi
;
2983 if (!FillDrawPixelInfo(&tmp_dpi
, ir
)) return;
2984 AutoRestoreBackup
dpi_backup(_cur_dpi
, &tmp_dpi
);
2986 int left_pos
= WidgetDimensions::scaled
.frametext
.left
- WidgetDimensions::scaled
.bevel
.left
;
2987 if (this->ind_cargo
>= NUM_INDUSTRYTYPES
) left_pos
+= (CargoesField::industry_width
+ CargoesField::cargo_field_width
) / 2;
2988 int last_column
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 4 : 2;
2990 const NWidgetBase
*nwp
= this->GetWidget
<NWidgetBase
>(WID_IC_PANEL
);
2991 int vpos
= WidgetDimensions::scaled
.frametext
.top
- WidgetDimensions::scaled
.bevel
.top
- this->vscroll
->GetPosition() * nwp
->resize_y
;
2992 int row_height
= CargoesField::small_height
;
2993 for (const auto &field
: this->fields
) {
2994 if (vpos
+ row_height
>= 0) {
2995 int xpos
= left_pos
;
2997 if (_current_text_dir
== TD_RTL
) {
3004 while (col
>= 0 && col
<= last_column
) {
3005 field
.columns
[col
].Draw(xpos
, vpos
);
3006 xpos
+= (col
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
;
3011 if (vpos
>= height
) break;
3012 row_height
= CargoesField::normal_height
;
3017 * Calculate in which field was clicked, and within the field, at what position.
3018 * @param pt Clicked position in the #WID_IC_PANEL widget.
3019 * @param fieldxy If \c true is returned, field x/y coordinate of \a pt.
3020 * @param xy If \c true is returned, x/y coordinate with in the field.
3021 * @return Clicked at a valid position.
3023 bool CalculatePositionInWidget(Point pt
, Point
*fieldxy
, Point
*xy
)
3025 const NWidgetBase
*nw
= this->GetWidget
<NWidgetBase
>(WID_IC_PANEL
);
3029 int vpos
= WidgetDimensions::scaled
.frametext
.top
+ CargoesField::small_height
- this->vscroll
->GetPosition() * nw
->resize_y
;
3030 if (pt
.y
< vpos
) return false;
3032 int row
= (pt
.y
- vpos
) / CargoesField::normal_height
; // row is relative to row 1.
3033 if (row
+ 1 >= (int)this->fields
.size()) return false;
3034 vpos
= pt
.y
- vpos
- row
* CargoesField::normal_height
; // Position in the row + 1 field
3035 row
++; // rebase row to match index of this->fields.
3037 int xpos
= 2 * WidgetDimensions::scaled
.frametext
.left
+ ((this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 0 : (CargoesField::industry_width
+ CargoesField::cargo_field_width
) / 2);
3038 if (pt
.x
< xpos
) return false;
3040 for (column
= 0; column
<= 5; column
++) {
3041 int width
= (column
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
;
3042 if (pt
.x
< xpos
+ width
) break;
3045 int num_columns
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 4 : 2;
3046 if (column
> num_columns
) return false;
3049 /* Return both positions, compensating for RTL languages (which works due to the equal symmetry in both displays). */
3052 if (_current_text_dir
== TD_RTL
) {
3053 fieldxy
->x
= num_columns
- column
;
3054 xy
->x
= ((column
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
) - xpos
;
3056 fieldxy
->x
= column
;
3062 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
3065 case WID_IC_PANEL
: {
3067 if (!CalculatePositionInWidget(pt
, &fieldxy
, &xy
)) return;
3069 const CargoesField
*fld
= this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
;
3070 switch (fld
->type
) {
3072 if (fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) this->ComputeIndustryDisplay(fld
->u
.industry
.ind_type
);
3076 CargoesField
*lft
= (fieldxy
.x
> 0) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
- 1 : nullptr;
3077 CargoesField
*rgt
= (fieldxy
.x
< 4) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
+ 1 : nullptr;
3078 CargoID cid
= fld
->CargoClickedAt(lft
, rgt
, xy
);
3079 if (IsValidCargoID(cid
)) this->ComputeCargoDisplay(cid
);
3083 case CFT_CARGO_LABEL
: {
3084 CargoID cid
= fld
->CargoLabelClickedAt(xy
);
3085 if (IsValidCargoID(cid
)) this->ComputeCargoDisplay(cid
);
3096 this->ToggleWidgetLoweredState(WID_IC_NOTIFY
);
3097 this->SetWidgetDirty(WID_IC_NOTIFY
);
3098 if (_settings_client
.sound
.click_beep
) SndPlayFx(SND_15_BEEP
);
3100 if (this->IsWidgetLowered(WID_IC_NOTIFY
)) {
3101 if (FindWindowByClass(WC_SMALLMAP
) == nullptr) ShowSmallMap();
3102 this->NotifySmallmap();
3106 case WID_IC_CARGO_DROPDOWN
: {
3108 Dimension d
= GetLargestCargoIconSize();
3109 for (const CargoSpec
*cs
: _sorted_standard_cargo_specs
) {
3110 lst
.push_back(MakeDropDownListIconItem(d
, cs
->GetCargoIcon(), PAL_NONE
, cs
->name
, cs
->Index()));
3113 int selected
= (this->ind_cargo
>= NUM_INDUSTRYTYPES
) ? (int)(this->ind_cargo
- NUM_INDUSTRYTYPES
) : -1;
3114 ShowDropDownList(this, std::move(lst
), selected
, WID_IC_CARGO_DROPDOWN
);
3119 case WID_IC_IND_DROPDOWN
: {
3121 for (IndustryType ind
: _sorted_industry_types
) {
3122 const IndustrySpec
*indsp
= GetIndustrySpec(ind
);
3123 if (!indsp
->enabled
) continue;
3124 lst
.push_back(MakeDropDownListStringItem(indsp
->name
, ind
));
3127 int selected
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? (int)this->ind_cargo
: -1;
3128 ShowDropDownList(this, std::move(lst
), selected
, WID_IC_IND_DROPDOWN
);
3135 void OnDropdownSelect(WidgetID widget
, int index
) override
3137 if (index
< 0) return;
3140 case WID_IC_CARGO_DROPDOWN
:
3141 this->ComputeCargoDisplay(index
);
3144 case WID_IC_IND_DROPDOWN
:
3145 this->ComputeIndustryDisplay(index
);
3150 bool OnTooltip([[maybe_unused
]] Point pt
, WidgetID widget
, TooltipCloseCondition close_cond
) override
3152 if (widget
!= WID_IC_PANEL
) return false;
3155 if (!CalculatePositionInWidget(pt
, &fieldxy
, &xy
)) return false;
3157 const CargoesField
*fld
= this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
;
3158 CargoID cid
= INVALID_CARGO
;
3159 switch (fld
->type
) {
3161 CargoesField
*lft
= (fieldxy
.x
> 0) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
- 1 : nullptr;
3162 CargoesField
*rgt
= (fieldxy
.x
< 4) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
+ 1 : nullptr;
3163 cid
= fld
->CargoClickedAt(lft
, rgt
, xy
);
3167 case CFT_CARGO_LABEL
: {
3168 cid
= fld
->CargoLabelClickedAt(xy
);
3173 if (fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
&& (this->ind_cargo
>= NUM_INDUSTRYTYPES
|| fieldxy
.x
!= 2)) {
3174 GuiShowTooltips(this, STR_INDUSTRY_CARGOES_INDUSTRY_TOOLTIP
, close_cond
);
3181 if (IsValidCargoID(cid
) && (this->ind_cargo
< NUM_INDUSTRYTYPES
|| cid
!= this->ind_cargo
- NUM_INDUSTRYTYPES
)) {
3182 const CargoSpec
*csp
= CargoSpec::Get(cid
);
3183 SetDParam(0, csp
->name
);
3184 GuiShowTooltips(this, STR_INDUSTRY_CARGOES_CARGO_TOOLTIP
, close_cond
, 1);
3191 void OnResize() override
3193 this->vscroll
->SetCapacityFromWidget(this, WID_IC_PANEL
, WidgetDimensions::scaled
.framerect
.Vertical() + CargoesField::small_height
);
3198 * Open the industry and cargoes window.
3199 * @param id Industry type to display, \c NUM_INDUSTRYTYPES selects a default industry type.
3201 static void ShowIndustryCargoesWindow(IndustryType id
)
3203 if (id
>= NUM_INDUSTRYTYPES
) {
3204 for (IndustryType ind
: _sorted_industry_types
) {
3205 const IndustrySpec
*indsp
= GetIndustrySpec(ind
);
3206 if (indsp
->enabled
) {
3211 if (id
>= NUM_INDUSTRYTYPES
) return;
3214 Window
*w
= BringWindowToFrontById(WC_INDUSTRY_CARGOES
, 0);
3216 w
->InvalidateData(id
);
3219 new IndustryCargoesWindow(id
);
3222 /** Open the industry and cargoes window with an industry. */
3223 void ShowIndustryCargoesWindow()
3225 ShowIndustryCargoesWindow(NUM_INDUSTRYTYPES
);