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 AppendStringInPlace(cargostring
, 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
= ScrollRect(ir
, *this->hscroll
, 1);
1717 if (this->industries
.empty()) {
1718 DrawString(ir
, STR_INDUSTRY_DIRECTORY_NONE
);
1721 const CargoID acf_cid
= this->accepted_cargo_filter_criteria
;
1722 auto [first
, last
] = this->vscroll
->GetVisibleRangeIterators(this->industries
);
1723 for (auto it
= first
; it
!= last
; ++it
) {
1724 TextColour tc
= TC_FROMSTRING
;
1725 if (acf_cid
!= CargoFilterCriteria::CF_ANY
&& acf_cid
!= CargoFilterCriteria::CF_NONE
) {
1726 Industry
*ind
= const_cast<Industry
*>(*it
);
1727 if (IndustryTemporarilyRefusesCargo(ind
, acf_cid
)) {
1728 tc
= TC_GREY
| TC_FORCED
;
1731 DrawString(ir
, this->GetIndustryString(*it
), tc
);
1733 ir
.top
+= this->resize
.step_height
;
1740 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
1743 case WID_ID_DROPDOWN_ORDER
: {
1744 Dimension d
= GetStringBoundingBox(this->GetWidget
<NWidgetCore
>(widget
)->widget_data
);
1745 d
.width
+= padding
.width
+ Window::SortButtonWidth() * 2; // Doubled since the string is centred and it also looks better.
1746 d
.height
+= padding
.height
;
1747 size
= maxdim(size
, d
);
1751 case WID_ID_DROPDOWN_CRITERIA
: {
1752 Dimension d
= GetStringListBoundingBox(IndustryDirectoryWindow::sorter_names
);
1753 d
.width
+= padding
.width
;
1754 d
.height
+= padding
.height
;
1755 size
= maxdim(size
, d
);
1759 case WID_ID_INDUSTRY_LIST
: {
1760 Dimension d
= GetStringBoundingBox(STR_INDUSTRY_DIRECTORY_NONE
);
1761 resize
.height
= d
.height
;
1763 d
.width
+= padding
.width
;
1764 d
.height
+= padding
.height
;
1765 size
= maxdim(size
, d
);
1771 DropDownList
BuildCargoDropDownList() const
1775 /* Add item for disabling filtering. */
1776 list
.push_back(MakeDropDownListStringItem(this->GetCargoFilterLabel(CargoFilterCriteria::CF_ANY
), CargoFilterCriteria::CF_ANY
));
1777 /* Add item for industries not producing anything, e.g. power plants */
1778 list
.push_back(MakeDropDownListStringItem(this->GetCargoFilterLabel(CargoFilterCriteria::CF_NONE
), CargoFilterCriteria::CF_NONE
));
1781 Dimension d
= GetLargestCargoIconSize();
1782 for (const CargoSpec
*cs
: _sorted_standard_cargo_specs
) {
1783 list
.push_back(MakeDropDownListIconItem(d
, cs
->GetCargoIcon(), PAL_NONE
, cs
->name
, cs
->Index()));
1789 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
1792 case WID_ID_DROPDOWN_ORDER
:
1793 this->industries
.ToggleSortOrder();
1797 case WID_ID_DROPDOWN_CRITERIA
:
1798 ShowDropDownMenu(this, IndustryDirectoryWindow::sorter_names
, this->industries
.SortType(), WID_ID_DROPDOWN_CRITERIA
, 0, 0);
1801 case WID_ID_FILTER_BY_ACC_CARGO
: // Cargo filter dropdown
1802 ShowDropDownList(this, this->BuildCargoDropDownList(), this->accepted_cargo_filter_criteria
, widget
);
1805 case WID_ID_FILTER_BY_PROD_CARGO
: // Cargo filter dropdown
1806 ShowDropDownList(this, this->BuildCargoDropDownList(), this->produced_cargo_filter_criteria
, widget
);
1809 case WID_ID_INDUSTRY_LIST
: {
1810 auto it
= this->vscroll
->GetScrolledItemFromWidget(this->industries
, pt
.y
, this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.top
);
1811 if (it
!= this->industries
.end()) {
1812 if (_ctrl_pressed
) {
1813 ShowExtraViewportWindow((*it
)->location
.tile
);
1815 ScrollMainWindowToTile((*it
)->location
.tile
);
1823 void OnDropdownSelect(WidgetID widget
, int index
) override
1826 case WID_ID_DROPDOWN_CRITERIA
: {
1827 if (this->industries
.SortType() != index
) {
1828 this->industries
.SetSortType(index
);
1829 this->BuildSortIndustriesList();
1834 case WID_ID_FILTER_BY_ACC_CARGO
: {
1835 this->SetAcceptedCargoFilter(index
);
1836 this->BuildSortIndustriesList();
1840 case WID_ID_FILTER_BY_PROD_CARGO
: {
1841 this->SetProducedCargoFilter(index
);
1842 this->BuildSortIndustriesList();
1848 void OnResize() override
1850 this->vscroll
->SetCapacityFromWidget(this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.Vertical());
1851 this->hscroll
->SetCapacityFromWidget(this, WID_ID_INDUSTRY_LIST
, WidgetDimensions::scaled
.framerect
.Horizontal());
1854 void OnEditboxChanged(WidgetID wid
) override
1856 if (wid
== WID_ID_FILTER
) {
1857 this->string_filter
.SetFilterTerm(this->industry_editbox
.text
.buf
);
1858 this->InvalidateData(IDIWD_FORCE_REBUILD
);
1862 void OnPaint() override
1864 if (this->industries
.NeedRebuild()) this->BuildSortIndustriesList();
1865 this->DrawWidgets();
1868 /** Rebuild the industry list on a regular interval. */
1869 IntervalTimer
<TimerWindow
> rebuild_interval
= {std::chrono::seconds(3), [this](auto) {
1870 this->industries
.ForceResort();
1871 this->BuildSortIndustriesList();
1875 * Some data on this window has become invalid.
1876 * @param data Information about the changed data.
1877 * @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.
1879 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
1882 case IDIWD_FORCE_REBUILD
:
1883 /* This needs to be done in command-scope to enforce rebuilding before resorting invalid data */
1884 this->industries
.ForceRebuild();
1887 case IDIWD_PRODUCTION_CHANGE
:
1888 if (this->industries
.SortType() == 2) this->industries
.ForceResort();
1892 this->industries
.ForceResort();
1897 EventState
OnHotkey(int hotkey
) override
1900 case IDHK_FOCUS_FILTER_BOX
:
1901 this->SetFocusedWidget(WID_ID_FILTER
);
1902 SetFocusedWindow(this); // The user has asked to give focus to the text box, so make sure this window is focused.
1905 return ES_NOT_HANDLED
;
1910 static inline HotkeyList hotkeys
{"industrydirectory", {
1911 Hotkey('F', "focus_filter_box", IDHK_FOCUS_FILTER_BOX
),
1915 Listing
IndustryDirectoryWindow::last_sorting
= {false, 0};
1917 /* Available station sorting functions. */
1918 const std::initializer_list
<GUIIndustryList::SortFunction
* const> IndustryDirectoryWindow::sorter_funcs
= {
1919 &IndustryNameSorter
,
1920 &IndustryTypeSorter
,
1921 &IndustryProductionSorter
,
1922 &IndustryTransportedCargoSorter
1925 CargoID
IndustryDirectoryWindow::produced_cargo_filter
= CargoFilterCriteria::CF_ANY
;
1928 /** Window definition of the industry directory gui */
1929 static WindowDesc
_industry_directory_desc(
1930 WDP_AUTO
, "list_industries", 428, 190,
1931 WC_INDUSTRY_DIRECTORY
, WC_NONE
,
1933 _nested_industry_directory_widgets
,
1934 &IndustryDirectoryWindow::hotkeys
1937 void ShowIndustryDirectory()
1939 AllocateWindowDescFront
<IndustryDirectoryWindow
>(_industry_directory_desc
, 0);
1942 /** Widgets of the industry cargoes window. */
1943 static constexpr NWidgetPart _nested_industry_cargoes_widgets
[] = {
1944 NWidget(NWID_HORIZONTAL
),
1945 NWidget(WWT_CLOSEBOX
, COLOUR_BROWN
),
1946 NWidget(WWT_CAPTION
, COLOUR_BROWN
, WID_IC_CAPTION
), SetDataTip(STR_INDUSTRY_CARGOES_INDUSTRY_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
1947 NWidget(WWT_SHADEBOX
, COLOUR_BROWN
),
1948 NWidget(WWT_DEFSIZEBOX
, COLOUR_BROWN
),
1949 NWidget(WWT_STICKYBOX
, COLOUR_BROWN
),
1951 NWidget(NWID_HORIZONTAL
),
1952 NWidget(WWT_PANEL
, COLOUR_BROWN
, WID_IC_PANEL
), SetResize(1, 10), SetScrollbar(WID_IC_SCROLLBAR
), EndContainer(),
1953 NWidget(NWID_VSCROLLBAR
, COLOUR_BROWN
, WID_IC_SCROLLBAR
),
1955 NWidget(NWID_HORIZONTAL
),
1956 NWidget(WWT_TEXTBTN
, COLOUR_BROWN
, WID_IC_NOTIFY
),
1957 SetDataTip(STR_INDUSTRY_CARGOES_NOTIFY_SMALLMAP
, STR_INDUSTRY_CARGOES_NOTIFY_SMALLMAP_TOOLTIP
),
1958 NWidget(WWT_PANEL
, COLOUR_BROWN
), SetFill(1, 0), SetResize(0, 0), EndContainer(),
1959 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_IC_IND_DROPDOWN
), SetFill(0, 0), SetResize(0, 0),
1960 SetDataTip(STR_INDUSTRY_CARGOES_SELECT_INDUSTRY
, STR_INDUSTRY_CARGOES_SELECT_INDUSTRY_TOOLTIP
),
1961 NWidget(WWT_DROPDOWN
, COLOUR_BROWN
, WID_IC_CARGO_DROPDOWN
), SetFill(0, 0), SetResize(0, 0),
1962 SetDataTip(STR_INDUSTRY_CARGOES_SELECT_CARGO
, STR_INDUSTRY_CARGOES_SELECT_CARGO_TOOLTIP
),
1963 NWidget(WWT_RESIZEBOX
, COLOUR_BROWN
),
1967 /** Window description for the industry cargoes window. */
1968 static WindowDesc
_industry_cargoes_desc(
1969 WDP_AUTO
, "industry_cargoes", 300, 210,
1970 WC_INDUSTRY_CARGOES
, WC_NONE
,
1972 _nested_industry_cargoes_widgets
1975 /** Available types of field. */
1976 enum CargoesFieldType
{
1977 CFT_EMPTY
, ///< Empty field.
1978 CFT_SMALL_EMPTY
, ///< Empty small field (for the header).
1979 CFT_INDUSTRY
, ///< Display industry.
1980 CFT_CARGO
, ///< Display cargo connections.
1981 CFT_CARGO_LABEL
, ///< Display cargo labels.
1982 CFT_HEADER
, ///< Header text.
1985 static const uint MAX_CARGOES
= 16; ///< Maximum number of cargoes carried in a #CFT_CARGO field in #CargoesField.
1987 /** Data about a single field in the #IndustryCargoesWindow panel. */
1988 struct CargoesField
{
1989 static int vert_inter_industry_space
;
1990 static int blob_distance
;
1992 static Dimension legend
;
1993 static Dimension cargo_border
;
1994 static Dimension cargo_line
;
1995 static Dimension cargo_space
;
1996 static Dimension cargo_stub
;
1998 static const int INDUSTRY_LINE_COLOUR
;
1999 static const int CARGO_LINE_COLOUR
;
2001 static int small_height
, normal_height
;
2002 static int cargo_field_width
;
2003 static int industry_width
;
2004 static uint max_cargoes
;
2006 using Cargoes
= uint16_t;
2007 static_assert(std::numeric_limits
<Cargoes
>::digits
>= MAX_CARGOES
);
2009 CargoesFieldType type
; ///< Type of field.
2012 IndustryType ind_type
; ///< Industry type (#NUM_INDUSTRYTYPES means 'houses').
2013 CargoID other_produced
[MAX_CARGOES
]; ///< Cargoes produced but not used in this figure.
2014 CargoID other_accepted
[MAX_CARGOES
]; ///< Cargoes accepted but not used in this figure.
2015 } industry
; ///< Industry data (for #CFT_INDUSTRY).
2017 CargoID vertical_cargoes
[MAX_CARGOES
]; ///< Cargoes running from top to bottom (cargo ID or #INVALID_CARGO).
2018 Cargoes supp_cargoes
; ///< Cargoes in \c vertical_cargoes entering from the left.
2019 Cargoes cust_cargoes
; ///< Cargoes in \c vertical_cargoes leaving to the right.
2020 uint8_t num_cargoes
; ///< Number of cargoes.
2021 uint8_t top_end
; ///< Stop at the top of the vertical cargoes.
2022 uint8_t bottom_end
; ///< Stop at the bottom of the vertical cargoes.
2023 } cargo
; ///< Cargo data (for #CFT_CARGO).
2025 CargoID cargoes
[MAX_CARGOES
]; ///< Cargoes to display (or #INVALID_CARGO).
2026 bool left_align
; ///< Align all cargo texts to the left (else align to the right).
2027 } cargo_label
; ///< Label data (for #CFT_CARGO_LABEL).
2028 StringID header
; ///< Header text (for #CFT_HEADER).
2029 } u
; // Data for each type.
2032 * Make one of the empty fields (#CFT_EMPTY or #CFT_SMALL_EMPTY).
2033 * @param type Type of empty field.
2035 void MakeEmpty(CargoesFieldType type
)
2041 * Make an industry type field.
2042 * @param ind_type Industry type (#NUM_INDUSTRYTYPES means 'houses').
2043 * @note #other_accepted and #other_produced should be filled later.
2045 void MakeIndustry(IndustryType ind_type
)
2047 this->type
= CFT_INDUSTRY
;
2048 this->u
.industry
.ind_type
= ind_type
;
2049 std::fill(std::begin(this->u
.industry
.other_accepted
), std::end(this->u
.industry
.other_accepted
), INVALID_CARGO
);
2050 std::fill(std::begin(this->u
.industry
.other_produced
), std::end(this->u
.industry
.other_produced
), INVALID_CARGO
);
2054 * Connect a cargo from an industry to the #CFT_CARGO column.
2055 * @param cargo Cargo to connect.
2056 * @param producer Cargo is produced (if \c false, cargo is assumed to be accepted).
2057 * @return Horizontal connection index, or \c -1 if not accepted at all.
2059 int ConnectCargo(CargoID cargo
, bool producer
)
2061 assert(this->type
== CFT_CARGO
);
2062 if (!IsValidCargoID(cargo
)) return -1;
2064 /* Find the vertical cargo column carrying the cargo. */
2066 for (int i
= 0; i
< this->u
.cargo
.num_cargoes
; i
++) {
2067 if (cargo
== this->u
.cargo
.vertical_cargoes
[i
]) {
2072 if (column
< 0) return -1;
2075 assert(!HasBit(this->u
.cargo
.supp_cargoes
, column
));
2076 SetBit(this->u
.cargo
.supp_cargoes
, column
);
2078 assert(!HasBit(this->u
.cargo
.cust_cargoes
, column
));
2079 SetBit(this->u
.cargo
.cust_cargoes
, column
);
2085 * Does this #CFT_CARGO field have a horizontal connection?
2086 * @return \c true if a horizontal connection exists, \c false otherwise.
2088 bool HasConnection()
2090 assert(this->type
== CFT_CARGO
);
2092 return this->u
.cargo
.supp_cargoes
!= 0 || this->u
.cargo
.cust_cargoes
!= 0;
2096 * Make a piece of cargo column.
2097 * @param cargoes Span of #CargoID (may contain #INVALID_CARGO).
2098 * @note #supp_cargoes and #cust_cargoes should be filled in later.
2100 void MakeCargo(const std::span
<const CargoID
> cargoes
)
2102 this->type
= CFT_CARGO
;
2103 assert(std::size(cargoes
) <= std::size(this->u
.cargo
.vertical_cargoes
));
2104 auto insert
= std::copy_if(std::begin(cargoes
), std::end(cargoes
), std::begin(this->u
.cargo
.vertical_cargoes
), IsValidCargoID
);
2105 this->u
.cargo
.num_cargoes
= static_cast<uint8_t>(std::distance(std::begin(this->u
.cargo
.vertical_cargoes
), insert
));
2106 CargoIDComparator comparator
;
2107 std::sort(std::begin(this->u
.cargo
.vertical_cargoes
), insert
, comparator
);
2108 std::fill(insert
, std::end(this->u
.cargo
.vertical_cargoes
), INVALID_CARGO
);
2109 this->u
.cargo
.top_end
= false;
2110 this->u
.cargo
.bottom_end
= false;
2111 this->u
.cargo
.supp_cargoes
= 0;
2112 this->u
.cargo
.cust_cargoes
= 0;
2116 * Make a field displaying cargo type names.
2117 * @param cargoes Span of #CargoID (may contain #INVALID_CARGO).
2118 * @param left_align ALign texts to the left (else to the right).
2120 void MakeCargoLabel(const std::span
<const CargoID
> cargoes
, bool left_align
)
2122 this->type
= CFT_CARGO_LABEL
;
2123 assert(std::size(cargoes
) <= std::size(this->u
.cargo_label
.cargoes
));
2124 auto insert
= std::copy(std::begin(cargoes
), std::end(cargoes
), std::begin(this->u
.cargo_label
.cargoes
));
2125 std::fill(insert
, std::end(this->u
.cargo_label
.cargoes
), INVALID_CARGO
);
2126 this->u
.cargo_label
.left_align
= left_align
;
2130 * Make a header above an industry column.
2131 * @param textid Text to display.
2133 void MakeHeader(StringID textid
)
2135 this->type
= CFT_HEADER
;
2136 this->u
.header
= textid
;
2140 * For a #CFT_CARGO, compute the left position of the left-most vertical cargo connection.
2141 * @param xpos Left position of the field.
2142 * @return Left position of the left-most vertical cargo column.
2144 int GetCargoBase(int xpos
) const
2146 assert(this->type
== CFT_CARGO
);
2147 int n
= this->u
.cargo
.num_cargoes
;
2149 return xpos
+ cargo_field_width
/ 2 - (CargoesField::cargo_line
.width
* n
+ CargoesField::cargo_space
.width
* (n
- 1)) / 2;
2154 * @param xpos Position of the left edge.
2155 * @param ypos Position of the top edge.
2157 void Draw(int xpos
, int ypos
) const
2159 switch (this->type
) {
2161 case CFT_SMALL_EMPTY
:
2165 ypos
+= (small_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
2166 DrawString(xpos
, xpos
+ industry_width
, ypos
, this->u
.header
, TC_WHITE
, SA_HOR_CENTER
);
2169 case CFT_INDUSTRY
: {
2170 int ypos1
= ypos
+ vert_inter_industry_space
/ 2;
2171 int ypos2
= ypos
+ normal_height
- 1 - vert_inter_industry_space
/ 2;
2172 int xpos2
= xpos
+ industry_width
- 1;
2173 DrawRectOutline({xpos
, ypos1
, xpos2
, ypos2
}, INDUSTRY_LINE_COLOUR
);
2174 ypos
+= (normal_height
- GetCharacterHeight(FS_NORMAL
)) / 2;
2175 if (this->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2176 const IndustrySpec
*indsp
= GetIndustrySpec(this->u
.industry
.ind_type
);
2177 DrawString(xpos
, xpos2
, ypos
, indsp
->name
, TC_WHITE
, SA_HOR_CENTER
);
2179 /* Draw the industry legend. */
2180 int blob_left
, blob_right
;
2181 if (_current_text_dir
== TD_RTL
) {
2182 blob_right
= xpos2
- blob_distance
;
2183 blob_left
= blob_right
- CargoesField::legend
.width
;
2185 blob_left
= xpos
+ blob_distance
;
2186 blob_right
= blob_left
+ CargoesField::legend
.width
;
2188 GfxFillRect(blob_left
, ypos2
- blob_distance
- CargoesField::legend
.height
, blob_right
, ypos2
- blob_distance
, PC_BLACK
); // Border
2189 GfxFillRect(blob_left
+ 1, ypos2
- blob_distance
- CargoesField::legend
.height
+ 1, blob_right
- 1, ypos2
- blob_distance
- 1, indsp
->map_colour
);
2191 DrawString(xpos
, xpos2
, ypos
, STR_INDUSTRY_CARGOES_HOUSES
, TC_FROMSTRING
, SA_HOR_CENTER
);
2194 /* Draw the other_produced/other_accepted cargoes. */
2195 std::span
<const CargoID
> other_right
, other_left
;
2196 if (_current_text_dir
== TD_RTL
) {
2197 other_right
= this->u
.industry
.other_accepted
;
2198 other_left
= this->u
.industry
.other_produced
;
2200 other_right
= this->u
.industry
.other_produced
;
2201 other_left
= this->u
.industry
.other_accepted
;
2203 ypos1
+= CargoesField::cargo_border
.height
+ (GetCharacterHeight(FS_NORMAL
) - CargoesField::cargo_line
.height
) / 2;
2204 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2205 if (IsValidCargoID(other_right
[i
])) {
2206 const CargoSpec
*csp
= CargoSpec::Get(other_right
[i
]);
2207 int xp
= xpos
+ industry_width
+ CargoesField::cargo_stub
.width
;
2208 DrawHorConnection(xpos
+ industry_width
, xp
- 1, ypos1
, csp
);
2209 GfxDrawLine(xp
, ypos1
, xp
, ypos1
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2211 if (IsValidCargoID(other_left
[i
])) {
2212 const CargoSpec
*csp
= CargoSpec::Get(other_left
[i
]);
2213 int xp
= xpos
- CargoesField::cargo_stub
.width
;
2214 DrawHorConnection(xp
+ 1, xpos
- 1, ypos1
, csp
);
2215 GfxDrawLine(xp
, ypos1
, xp
, ypos1
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2217 ypos1
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2223 int cargo_base
= this->GetCargoBase(xpos
);
2224 int top
= ypos
+ (this->u
.cargo
.top_end
? vert_inter_industry_space
/ 2 + 1 : 0);
2225 int bot
= ypos
- (this->u
.cargo
.bottom_end
? vert_inter_industry_space
/ 2 + 1 : 0) + normal_height
- 1;
2226 int colpos
= cargo_base
;
2227 for (int i
= 0; i
< this->u
.cargo
.num_cargoes
; i
++) {
2228 if (this->u
.cargo
.top_end
) GfxDrawLine(colpos
, top
- 1, colpos
+ CargoesField::cargo_line
.width
- 1, top
- 1, CARGO_LINE_COLOUR
);
2229 if (this->u
.cargo
.bottom_end
) GfxDrawLine(colpos
, bot
+ 1, colpos
+ CargoesField::cargo_line
.width
- 1, bot
+ 1, CARGO_LINE_COLOUR
);
2230 GfxDrawLine(colpos
, top
, colpos
, bot
, CARGO_LINE_COLOUR
);
2232 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[i
]);
2233 GfxFillRect(colpos
, top
, colpos
+ CargoesField::cargo_line
.width
- 2, bot
, csp
->legend_colour
, FILLRECT_OPAQUE
);
2234 colpos
+= CargoesField::cargo_line
.width
- 2;
2235 GfxDrawLine(colpos
, top
, colpos
, bot
, CARGO_LINE_COLOUR
);
2236 colpos
+= 1 + CargoesField::cargo_space
.width
;
2239 Cargoes hor_left
, hor_right
;
2240 if (_current_text_dir
== TD_RTL
) {
2241 hor_left
= this->u
.cargo
.cust_cargoes
;
2242 hor_right
= this->u
.cargo
.supp_cargoes
;
2244 hor_left
= this->u
.cargo
.supp_cargoes
;
2245 hor_right
= this->u
.cargo
.cust_cargoes
;
2247 ypos
+= CargoesField::cargo_border
.height
+ vert_inter_industry_space
/ 2 + (GetCharacterHeight(FS_NORMAL
) - CargoesField::cargo_line
.height
) / 2;
2248 for (uint i
= 0; i
< MAX_CARGOES
; i
++) {
2249 if (HasBit(hor_left
, i
)) {
2252 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[col
]);
2253 for (; col
> 0; col
--) {
2254 int lf
= cargo_base
+ col
* CargoesField::cargo_line
.width
+ (col
- 1) * CargoesField::cargo_space
.width
;
2255 DrawHorConnection(lf
, lf
+ CargoesField::cargo_space
.width
- dx
, ypos
, csp
);
2258 DrawHorConnection(xpos
, cargo_base
- dx
, ypos
, csp
);
2260 if (HasBit(hor_right
, i
)) {
2263 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo
.vertical_cargoes
[col
]);
2264 for (; col
< this->u
.cargo
.num_cargoes
- 1; col
++) {
2265 int lf
= cargo_base
+ (col
+ 1) * CargoesField::cargo_line
.width
+ col
* CargoesField::cargo_space
.width
;
2266 DrawHorConnection(lf
+ dx
- 1, lf
+ CargoesField::cargo_space
.width
- 1, ypos
, csp
);
2269 DrawHorConnection(cargo_base
+ col
* CargoesField::cargo_space
.width
+ (col
+ 1) * CargoesField::cargo_line
.width
- 1 + dx
, xpos
+ CargoesField::cargo_field_width
- 1, ypos
, csp
);
2271 ypos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2276 case CFT_CARGO_LABEL
:
2277 ypos
+= CargoesField::cargo_border
.height
+ vert_inter_industry_space
/ 2;
2278 for (uint i
= 0; i
< MAX_CARGOES
; i
++) {
2279 if (IsValidCargoID(this->u
.cargo_label
.cargoes
[i
])) {
2280 const CargoSpec
*csp
= CargoSpec::Get(this->u
.cargo_label
.cargoes
[i
]);
2281 DrawString(xpos
+ WidgetDimensions::scaled
.framerect
.left
, xpos
+ industry_width
- 1 - WidgetDimensions::scaled
.framerect
.right
, ypos
, csp
->name
, TC_WHITE
,
2282 (this->u
.cargo_label
.left_align
) ? SA_LEFT
: SA_RIGHT
);
2284 ypos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2294 * Decide which cargo was clicked at in a #CFT_CARGO field.
2295 * @param left Left industry neighbour if available (else \c nullptr should be supplied).
2296 * @param right Right industry neighbour if available (else \c nullptr should be supplied).
2297 * @param pt Click position in the cargo field.
2298 * @return Cargo clicked at, or #INVALID_CARGO if none.
2300 CargoID
CargoClickedAt(const CargoesField
*left
, const CargoesField
*right
, Point pt
) const
2302 assert(this->type
== CFT_CARGO
);
2304 /* Vertical matching. */
2305 int cpos
= this->GetCargoBase(0);
2307 for (col
= 0; col
< this->u
.cargo
.num_cargoes
; col
++) {
2308 if (pt
.x
< cpos
) break;
2309 if (pt
.x
< cpos
+ (int)CargoesField::cargo_line
.width
) return this->u
.cargo
.vertical_cargoes
[col
];
2310 cpos
+= CargoesField::cargo_line
.width
+ CargoesField::cargo_space
.width
;
2312 /* col = 0 -> left of first col, 1 -> left of 2nd col, ... this->u.cargo.num_cargoes right of last-col. */
2314 int vpos
= vert_inter_industry_space
/ 2 + CargoesField::cargo_border
.width
;
2316 for (row
= 0; row
< MAX_CARGOES
; row
++) {
2317 if (pt
.y
< vpos
) return INVALID_CARGO
;
2318 if (pt
.y
< vpos
+ GetCharacterHeight(FS_NORMAL
)) break;
2319 vpos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.width
;
2321 if (row
== MAX_CARGOES
) return INVALID_CARGO
;
2323 /* row = 0 -> at first horizontal row, row = 1 -> second horizontal row, 2 = 3rd horizontal row. */
2325 if (HasBit(this->u
.cargo
.supp_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2326 if (left
!= nullptr) {
2327 if (left
->type
== CFT_INDUSTRY
) return left
->u
.industry
.other_produced
[row
];
2328 if (left
->type
== CFT_CARGO_LABEL
&& !left
->u
.cargo_label
.left_align
) return left
->u
.cargo_label
.cargoes
[row
];
2330 return INVALID_CARGO
;
2332 if (col
== this->u
.cargo
.num_cargoes
) {
2333 if (HasBit(this->u
.cargo
.cust_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2334 if (right
!= nullptr) {
2335 if (right
->type
== CFT_INDUSTRY
) return right
->u
.industry
.other_accepted
[row
];
2336 if (right
->type
== CFT_CARGO_LABEL
&& right
->u
.cargo_label
.left_align
) return right
->u
.cargo_label
.cargoes
[row
];
2338 return INVALID_CARGO
;
2341 /* Clicked somewhere in-between vertical cargo connection.
2342 * Since the horizontal connection is made in the same order as the vertical list, the above condition
2343 * ensures we are left-below the main diagonal, thus at the supplying side.
2345 if (HasBit(this->u
.cargo
.supp_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2346 return INVALID_CARGO
;
2348 /* Clicked at a customer connection. */
2349 if (HasBit(this->u
.cargo
.cust_cargoes
, row
)) return this->u
.cargo
.vertical_cargoes
[row
];
2350 return INVALID_CARGO
;
2354 * Decide what cargo the user clicked in the cargo label field.
2355 * @param pt Click position in the cargo label field.
2356 * @return Cargo clicked at, or #INVALID_CARGO if none.
2358 CargoID
CargoLabelClickedAt(Point pt
) const
2360 assert(this->type
== CFT_CARGO_LABEL
);
2362 int vpos
= vert_inter_industry_space
/ 2 + CargoesField::cargo_border
.height
;
2364 for (row
= 0; row
< MAX_CARGOES
; row
++) {
2365 if (pt
.y
< vpos
) return INVALID_CARGO
;
2366 if (pt
.y
< vpos
+ GetCharacterHeight(FS_NORMAL
)) break;
2367 vpos
+= GetCharacterHeight(FS_NORMAL
) + CargoesField::cargo_space
.height
;
2369 if (row
== MAX_CARGOES
) return INVALID_CARGO
;
2370 return this->u
.cargo_label
.cargoes
[row
];
2375 * Draw a horizontal cargo connection.
2376 * @param left Left-most coordinate to draw.
2377 * @param right Right-most coordinate to draw.
2378 * @param top Top coordinate of the cargo connection.
2379 * @param csp Cargo to draw.
2381 static void DrawHorConnection(int left
, int right
, int top
, const CargoSpec
*csp
)
2383 GfxDrawLine(left
, top
, right
, top
, CARGO_LINE_COLOUR
);
2384 GfxFillRect(left
, top
+ 1, right
, top
+ CargoesField::cargo_line
.height
- 2, csp
->legend_colour
, FILLRECT_OPAQUE
);
2385 GfxDrawLine(left
, top
+ CargoesField::cargo_line
.height
- 1, right
, top
+ CargoesField::cargo_line
.height
- 1, CARGO_LINE_COLOUR
);
2389 static_assert(MAX_CARGOES
>= std::tuple_size_v
<decltype(IndustrySpec::produced_cargo
)>);
2390 static_assert(MAX_CARGOES
>= std::tuple_size_v
<decltype(IndustrySpec::accepts_cargo
)>);
2392 Dimension
CargoesField::legend
; ///< Dimension of the legend blob.
2393 Dimension
CargoesField::cargo_border
; ///< Dimensions of border between cargo lines and industry boxes.
2394 Dimension
CargoesField::cargo_line
; ///< Dimensions of cargo lines.
2395 Dimension
CargoesField::cargo_space
; ///< Dimensions of space between cargo lines.
2396 Dimension
CargoesField::cargo_stub
; ///< Dimensions of cargo stub (unconnected cargo line.)
2398 int CargoesField::small_height
; ///< Height of the header row.
2399 int CargoesField::normal_height
; ///< Height of the non-header rows.
2400 int CargoesField::industry_width
; ///< Width of an industry field.
2401 int CargoesField::cargo_field_width
; ///< Width of a cargo field.
2402 uint
CargoesField::max_cargoes
; ///< Largest number of cargoes actually on any industry.
2403 int CargoesField::vert_inter_industry_space
; ///< Amount of space between two industries in a column.
2405 int CargoesField::blob_distance
; ///< Distance of the industry legend colour from the edge of the industry box.
2407 const int CargoesField::INDUSTRY_LINE_COLOUR
= PC_YELLOW
; ///< Line colour of the industry type box.
2408 const int CargoesField::CARGO_LINE_COLOUR
= PC_YELLOW
; ///< Line colour around the cargo.
2410 /** A single row of #CargoesField. */
2412 CargoesField columns
[5]; ///< One row of fields.
2415 * Connect industry production cargoes to the cargo column after it.
2416 * @param column Column of the industry.
2418 void ConnectIndustryProduced(int column
)
2420 CargoesField
*ind_fld
= this->columns
+ column
;
2421 CargoesField
*cargo_fld
= this->columns
+ column
+ 1;
2422 assert(ind_fld
->type
== CFT_INDUSTRY
&& cargo_fld
->type
== CFT_CARGO
);
2424 std::fill(std::begin(ind_fld
->u
.industry
.other_produced
), std::end(ind_fld
->u
.industry
.other_produced
), INVALID_CARGO
);
2426 if (ind_fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2427 CargoID others
[MAX_CARGOES
]; // Produced cargoes not carried in the cargo column.
2428 int other_count
= 0;
2430 const IndustrySpec
*indsp
= GetIndustrySpec(ind_fld
->u
.industry
.ind_type
);
2431 assert(CargoesField::max_cargoes
<= std::size(indsp
->produced_cargo
));
2432 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2433 int col
= cargo_fld
->ConnectCargo(indsp
->produced_cargo
[i
], true);
2434 if (col
< 0) others
[other_count
++] = indsp
->produced_cargo
[i
];
2437 /* Allocate other cargoes in the empty holes of the horizontal cargo connections. */
2438 for (uint i
= 0; i
< CargoesField::max_cargoes
&& other_count
> 0; i
++) {
2439 if (HasBit(cargo_fld
->u
.cargo
.supp_cargoes
, i
)) ind_fld
->u
.industry
.other_produced
[i
] = others
[--other_count
];
2442 /* Houses only display cargo that towns produce. */
2443 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2444 CargoID cid
= cargo_fld
->u
.cargo
.vertical_cargoes
[i
];
2445 TownProductionEffect tpe
= CargoSpec::Get(cid
)->town_production_effect
;
2446 if (tpe
== TPE_PASSENGERS
|| tpe
== TPE_MAIL
) cargo_fld
->ConnectCargo(cid
, true);
2452 * Construct a #CFT_CARGO_LABEL field.
2453 * @param column Column to create the new field.
2454 * @param accepting Display accepted cargo (if \c false, display produced cargo).
2456 void MakeCargoLabel(int column
, bool accepting
)
2458 CargoID cargoes
[MAX_CARGOES
];
2459 std::fill(std::begin(cargoes
), std::end(cargoes
), INVALID_CARGO
);
2461 CargoesField
*label_fld
= this->columns
+ column
;
2462 CargoesField
*cargo_fld
= this->columns
+ (accepting
? column
- 1 : column
+ 1);
2464 assert(cargo_fld
->type
== CFT_CARGO
&& label_fld
->type
== CFT_EMPTY
);
2465 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2466 int col
= cargo_fld
->ConnectCargo(cargo_fld
->u
.cargo
.vertical_cargoes
[i
], !accepting
);
2467 if (col
>= 0) cargoes
[col
] = cargo_fld
->u
.cargo
.vertical_cargoes
[i
];
2469 label_fld
->MakeCargoLabel(cargoes
, accepting
);
2474 * Connect industry accepted cargoes to the cargo column before it.
2475 * @param column Column of the industry.
2477 void ConnectIndustryAccepted(int column
)
2479 CargoesField
*ind_fld
= this->columns
+ column
;
2480 CargoesField
*cargo_fld
= this->columns
+ column
- 1;
2481 assert(ind_fld
->type
== CFT_INDUSTRY
&& cargo_fld
->type
== CFT_CARGO
);
2483 std::fill(std::begin(ind_fld
->u
.industry
.other_accepted
), std::end(ind_fld
->u
.industry
.other_accepted
), INVALID_CARGO
);
2485 if (ind_fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) {
2486 CargoID others
[MAX_CARGOES
]; // Accepted cargoes not carried in the cargo column.
2487 int other_count
= 0;
2489 const IndustrySpec
*indsp
= GetIndustrySpec(ind_fld
->u
.industry
.ind_type
);
2490 assert(CargoesField::max_cargoes
<= std::size(indsp
->accepts_cargo
));
2491 for (uint i
= 0; i
< CargoesField::max_cargoes
; i
++) {
2492 int col
= cargo_fld
->ConnectCargo(indsp
->accepts_cargo
[i
], false);
2493 if (col
< 0) others
[other_count
++] = indsp
->accepts_cargo
[i
];
2496 /* Allocate other cargoes in the empty holes of the horizontal cargo connections. */
2497 for (uint i
= 0; i
< CargoesField::max_cargoes
&& other_count
> 0; i
++) {
2498 if (!HasBit(cargo_fld
->u
.cargo
.cust_cargoes
, i
)) ind_fld
->u
.industry
.other_accepted
[i
] = others
[--other_count
];
2501 /* Houses only display what is demanded. */
2502 for (uint i
= 0; i
< cargo_fld
->u
.cargo
.num_cargoes
; i
++) {
2503 for (const auto &hs
: HouseSpec::Specs()) {
2504 if (!hs
.enabled
) continue;
2506 for (uint j
= 0; j
< lengthof(hs
.accepts_cargo
); j
++) {
2507 if (hs
.cargo_acceptance
[j
] > 0 && cargo_fld
->u
.cargo
.vertical_cargoes
[i
] == hs
.accepts_cargo
[j
]) {
2508 cargo_fld
->ConnectCargo(cargo_fld
->u
.cargo
.vertical_cargoes
[i
], false);
2521 * Window displaying the cargo connections around an industry (or cargo).
2523 * The main display is constructed from 'fields', rectangles that contain an industry, piece of the cargo connection, cargo labels, or headers.
2524 * For a nice display, the following should be kept in mind:
2525 * - A #CFT_HEADER is always at the top of an column of #CFT_INDUSTRY fields.
2526 * - A #CFT_CARGO_LABEL field is also always put in a column of #CFT_INDUSTRY fields.
2527 * - The top row contains #CFT_HEADER and #CFT_SMALL_EMPTY fields.
2528 * - Cargo connections have a column of their own (#CFT_CARGO fields).
2529 * - 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.
2530 * The information however is part of the industry.
2532 * This results in the following invariants:
2533 * - Width of a #CFT_INDUSTRY column is large enough to hold all industry type labels, all cargo labels, and all header texts.
2534 * - Height of a #CFT_INDUSTRY is large enough to hold a header line, or a industry type line, \c N cargo labels
2535 * (where \c N is the maximum number of cargoes connected between industries), \c N connections of cargo types, and space
2536 * between two industry types (1/2 above it, and 1/2 underneath it).
2537 * - Width of a cargo field (#CFT_CARGO) is large enough to hold \c N vertical columns (one for each type of cargo).
2538 * Also, space is needed between an industry and the leftmost/rightmost column to draw the non-carried cargoes.
2539 * - Height of a #CFT_CARGO field is equally high as the height of the #CFT_INDUSTRY.
2540 * - A field at the top (#CFT_HEADER or #CFT_SMALL_EMPTY) match the width of the fields below them (#CFT_INDUSTRY respectively
2541 * #CFT_CARGO), the height should be sufficient to display the header text.
2543 * When displaying the cargoes around an industry type, five columns are needed (supplying industries, accepted cargoes, the industry,
2544 * produced cargoes, customer industries). Displaying the industries around a cargo needs three columns (supplying industries, the cargo,
2545 * 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.
2547 struct IndustryCargoesWindow
: public Window
{
2548 typedef std::vector
<CargoesRow
> Fields
;
2550 Fields fields
; ///< Fields to display in the #WID_IC_PANEL.
2551 uint ind_cargo
; ///< If less than #NUM_INDUSTRYTYPES, an industry type, else a cargo id + NUM_INDUSTRYTYPES.
2552 Dimension cargo_textsize
; ///< Size to hold any cargo text, as well as STR_INDUSTRY_CARGOES_SELECT_CARGO.
2553 Dimension ind_textsize
; ///< Size to hold any industry type text, as well as STR_INDUSTRY_CARGOES_SELECT_INDUSTRY.
2556 IndustryCargoesWindow(int id
) : Window(_industry_cargoes_desc
)
2559 this->CreateNestedTree();
2560 this->vscroll
= this->GetScrollbar(WID_IC_SCROLLBAR
);
2561 this->FinishInitNested(0);
2562 this->OnInvalidateData(id
);
2565 void OnInit() override
2567 /* Initialize static CargoesField size variables. */
2568 Dimension d
= GetStringBoundingBox(STR_INDUSTRY_CARGOES_PRODUCERS
);
2569 d
= maxdim(d
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_CUSTOMERS
));
2570 d
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
2571 d
.height
+= WidgetDimensions::scaled
.frametext
.Vertical();
2572 CargoesField::small_height
= d
.height
;
2574 /* Size of the legend blob -- slightly larger than the smallmap legend blob. */
2575 CargoesField::legend
.height
= GetCharacterHeight(FS_SMALL
);
2576 CargoesField::legend
.width
= CargoesField::legend
.height
* 9 / 6;
2578 /* Size of cargo lines. */
2579 CargoesField::cargo_line
.width
= ScaleGUITrad(6);
2580 CargoesField::cargo_line
.height
= CargoesField::cargo_line
.width
;
2582 /* Size of border between cargo lines and industry boxes. */
2583 CargoesField::cargo_border
.width
= CargoesField::cargo_line
.width
* 3 / 2;
2584 CargoesField::cargo_border
.height
= CargoesField::cargo_line
.width
/ 2;
2586 /* Size of space between cargo lines. */
2587 CargoesField::cargo_space
.width
= CargoesField::cargo_line
.width
/ 2;
2588 CargoesField::cargo_space
.height
= CargoesField::cargo_line
.height
/ 2;
2590 /* Size of cargo stub (unconnected cargo line.) */
2591 CargoesField::cargo_stub
.width
= CargoesField::cargo_line
.width
/ 2;
2592 CargoesField::cargo_stub
.height
= CargoesField::cargo_line
.height
; /* Unused */
2594 CargoesField::vert_inter_industry_space
= WidgetDimensions::scaled
.vsep_wide
;
2595 CargoesField::blob_distance
= WidgetDimensions::scaled
.hsep_normal
;
2597 /* Decide about the size of the box holding the text of an industry type. */
2598 this->ind_textsize
.width
= 0;
2599 this->ind_textsize
.height
= 0;
2600 CargoesField::max_cargoes
= 0;
2601 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2602 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2603 if (!indsp
->enabled
) continue;
2604 this->ind_textsize
= maxdim(this->ind_textsize
, GetStringBoundingBox(indsp
->name
));
2605 CargoesField::max_cargoes
= std::max
<uint
>(CargoesField::max_cargoes
, std::count_if(std::begin(indsp
->accepts_cargo
), std::end(indsp
->accepts_cargo
), IsValidCargoID
));
2606 CargoesField::max_cargoes
= std::max
<uint
>(CargoesField::max_cargoes
, std::count_if(std::begin(indsp
->produced_cargo
), std::end(indsp
->produced_cargo
), IsValidCargoID
));
2608 d
.width
= std::max(d
.width
, this->ind_textsize
.width
);
2609 d
.height
= this->ind_textsize
.height
;
2610 this->ind_textsize
= maxdim(this->ind_textsize
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_SELECT_INDUSTRY
));
2612 /* Compute max size of the cargo texts. */
2613 this->cargo_textsize
.width
= 0;
2614 this->cargo_textsize
.height
= 0;
2615 for (const CargoSpec
*csp
: CargoSpec::Iterate()) {
2616 if (!csp
->IsValid()) continue;
2617 this->cargo_textsize
= maxdim(this->cargo_textsize
, GetStringBoundingBox(csp
->name
));
2619 d
= maxdim(d
, this->cargo_textsize
); // Box must also be wide enough to hold any cargo label.
2620 this->cargo_textsize
= maxdim(this->cargo_textsize
, GetStringBoundingBox(STR_INDUSTRY_CARGOES_SELECT_CARGO
));
2622 d
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
2623 /* Ensure the height is enough for the industry type text, for the horizontal connections, and for the cargo labels. */
2624 uint min_ind_height
= CargoesField::cargo_border
.height
* 2 + CargoesField::max_cargoes
* GetCharacterHeight(FS_NORMAL
) + (CargoesField::max_cargoes
- 1) * CargoesField::cargo_space
.height
;
2625 d
.height
= std::max(d
.height
+ WidgetDimensions::scaled
.frametext
.Vertical(), min_ind_height
);
2627 CargoesField::industry_width
= d
.width
;
2628 CargoesField::normal_height
= d
.height
+ CargoesField::vert_inter_industry_space
;
2630 /* Width of a #CFT_CARGO field. */
2631 CargoesField::cargo_field_width
= CargoesField::cargo_border
.width
* 2 + CargoesField::cargo_line
.width
* CargoesField::max_cargoes
+ CargoesField::cargo_space
.width
* (CargoesField::max_cargoes
- 1);
2634 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
2638 resize
.height
= CargoesField::normal_height
;
2639 size
.width
= CargoesField::industry_width
* 3 + CargoesField::cargo_field_width
* 2 + WidgetDimensions::scaled
.frametext
.Horizontal();
2640 size
.height
= CargoesField::small_height
+ 2 * resize
.height
+ WidgetDimensions::scaled
.frametext
.Vertical();
2643 case WID_IC_IND_DROPDOWN
:
2644 size
.width
= std::max(size
.width
, this->ind_textsize
.width
+ padding
.width
);
2647 case WID_IC_CARGO_DROPDOWN
:
2648 size
.width
= std::max(size
.width
, this->cargo_textsize
.width
+ padding
.width
);
2653 void SetStringParameters(WidgetID widget
) const override
2655 if (widget
!= WID_IC_CAPTION
) return;
2657 if (this->ind_cargo
< NUM_INDUSTRYTYPES
) {
2658 const IndustrySpec
*indsp
= GetIndustrySpec(this->ind_cargo
);
2659 SetDParam(0, indsp
->name
);
2661 const CargoSpec
*csp
= CargoSpec::Get(this->ind_cargo
- NUM_INDUSTRYTYPES
);
2662 SetDParam(0, csp
->name
);
2667 * Do the two sets of cargoes have a valid cargo in common?
2668 * @param cargoes1 Span of the first cargo list.
2669 * @param cargoes2 Span of the second cargo list.
2670 * @return Arrays have at least one valid cargo in common.
2672 static bool HasCommonValidCargo(const std::span
<const CargoID
> cargoes1
, const std::span
<const CargoID
> cargoes2
)
2674 for (const CargoID cid1
: cargoes1
) {
2675 if (!IsValidCargoID(cid1
)) continue;
2676 for (const CargoID cid2
: cargoes2
) {
2677 if (cid1
== cid2
) return true;
2684 * Can houses be used to supply one of the cargoes?
2685 * @param cargoes Span of cargo list.
2686 * @return Houses can supply at least one of the cargoes.
2688 static bool HousesCanSupply(const std::span
<const CargoID
> cargoes
)
2690 for (const CargoID cid
: cargoes
) {
2691 if (!IsValidCargoID(cid
)) continue;
2692 TownProductionEffect tpe
= CargoSpec::Get(cid
)->town_production_effect
;
2693 if (tpe
== TPE_PASSENGERS
|| tpe
== TPE_MAIL
) return true;
2699 * Can houses be used as customers of the produced cargoes?
2700 * @param cargoes Span of cargo list.
2701 * @return Houses can accept at least one of the cargoes.
2703 static bool HousesCanAccept(const std::span
<const CargoID
> cargoes
)
2705 HouseZones climate_mask
;
2706 switch (_settings_game
.game_creation
.landscape
) {
2707 case LT_TEMPERATE
: climate_mask
= HZ_TEMP
; break;
2708 case LT_ARCTIC
: climate_mask
= HZ_SUBARTC_ABOVE
| HZ_SUBARTC_BELOW
; break;
2709 case LT_TROPIC
: climate_mask
= HZ_SUBTROPIC
; break;
2710 case LT_TOYLAND
: climate_mask
= HZ_TOYLND
; break;
2711 default: NOT_REACHED();
2713 for (const CargoID cid
: cargoes
) {
2714 if (!IsValidCargoID(cid
)) continue;
2716 for (const auto &hs
: HouseSpec::Specs()) {
2717 if (!hs
.enabled
|| !(hs
.building_availability
& climate_mask
)) continue;
2719 for (uint j
= 0; j
< lengthof(hs
.accepts_cargo
); j
++) {
2720 if (hs
.cargo_acceptance
[j
] > 0 && cid
== hs
.accepts_cargo
[j
]) return true;
2728 * Count how many industries have accepted cargoes in common with one of the supplied set.
2729 * @param cargoes Cargoes to search.
2730 * @return Number of industries that have an accepted cargo in common with the supplied set.
2732 static int CountMatchingAcceptingIndustries(const std::span
<const CargoID
> cargoes
)
2735 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2736 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2737 if (!indsp
->enabled
) continue;
2739 if (HasCommonValidCargo(cargoes
, indsp
->accepts_cargo
)) count
++;
2745 * Count how many industries have produced cargoes in common with one of the supplied set.
2746 * @param cargoes Cargoes to search.
2747 * @return Number of industries that have a produced cargo in common with the supplied set.
2749 static int CountMatchingProducingIndustries(const std::span
<const CargoID
> cargoes
)
2752 for (IndustryType it
= 0; it
< NUM_INDUSTRYTYPES
; it
++) {
2753 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2754 if (!indsp
->enabled
) continue;
2756 if (HasCommonValidCargo(cargoes
, indsp
->produced_cargo
)) count
++;
2762 * Shorten the cargo column to just the part between industries.
2763 * @param column Column number of the cargo column.
2764 * @param top Current top row.
2765 * @param bottom Current bottom row.
2767 void ShortenCargoColumn(int column
, int top
, int bottom
)
2769 while (top
< bottom
&& !this->fields
[top
].columns
[column
].HasConnection()) {
2770 this->fields
[top
].columns
[column
].MakeEmpty(CFT_EMPTY
);
2773 this->fields
[top
].columns
[column
].u
.cargo
.top_end
= true;
2775 while (bottom
> top
&& !this->fields
[bottom
].columns
[column
].HasConnection()) {
2776 this->fields
[bottom
].columns
[column
].MakeEmpty(CFT_EMPTY
);
2779 this->fields
[bottom
].columns
[column
].u
.cargo
.bottom_end
= true;
2783 * Place an industry in the fields.
2784 * @param row Row of the new industry.
2785 * @param col Column of the new industry.
2786 * @param it Industry to place.
2788 void PlaceIndustry(int row
, int col
, IndustryType it
)
2790 assert(this->fields
[row
].columns
[col
].type
== CFT_EMPTY
);
2791 this->fields
[row
].columns
[col
].MakeIndustry(it
);
2793 this->fields
[row
].ConnectIndustryProduced(col
);
2795 this->fields
[row
].ConnectIndustryAccepted(col
);
2800 * Notify smallmap that new displayed industries have been selected (in #_displayed_industries).
2802 void NotifySmallmap()
2804 if (!this->IsWidgetLowered(WID_IC_NOTIFY
)) return;
2806 /* Only notify the smallmap window if it exists. In particular, do not
2807 * bring it to the front to prevent messing up any nice layout of the user. */
2808 InvalidateWindowClassesData(WC_SMALLMAP
, 0);
2812 * Compute what and where to display for industry type \a it.
2813 * @param displayed_it Industry type to display.
2815 void ComputeIndustryDisplay(IndustryType displayed_it
)
2817 this->GetWidget
<NWidgetCore
>(WID_IC_CAPTION
)->widget_data
= STR_INDUSTRY_CARGOES_INDUSTRY_CAPTION
;
2818 this->ind_cargo
= displayed_it
;
2819 _displayed_industries
.reset();
2820 _displayed_industries
.set(displayed_it
);
2822 this->fields
.clear();
2823 CargoesRow
&first_row
= this->fields
.emplace_back();
2824 first_row
.columns
[0].MakeHeader(STR_INDUSTRY_CARGOES_PRODUCERS
);
2825 first_row
.columns
[1].MakeEmpty(CFT_SMALL_EMPTY
);
2826 first_row
.columns
[2].MakeEmpty(CFT_SMALL_EMPTY
);
2827 first_row
.columns
[3].MakeEmpty(CFT_SMALL_EMPTY
);
2828 first_row
.columns
[4].MakeHeader(STR_INDUSTRY_CARGOES_CUSTOMERS
);
2830 const IndustrySpec
*central_sp
= GetIndustrySpec(displayed_it
);
2831 bool houses_supply
= HousesCanSupply(central_sp
->accepts_cargo
);
2832 bool houses_accept
= HousesCanAccept(central_sp
->produced_cargo
);
2833 /* Make a field consisting of two cargo columns. */
2834 int num_supp
= CountMatchingProducingIndustries(central_sp
->accepts_cargo
) + houses_supply
;
2835 int num_cust
= CountMatchingAcceptingIndustries(central_sp
->produced_cargo
) + houses_accept
;
2836 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.
2837 for (int i
= 0; i
< num_indrows
; i
++) {
2838 CargoesRow
&row
= this->fields
.emplace_back();
2839 row
.columns
[0].MakeEmpty(CFT_EMPTY
);
2840 row
.columns
[1].MakeCargo(central_sp
->accepts_cargo
);
2841 row
.columns
[2].MakeEmpty(CFT_EMPTY
);
2842 row
.columns
[3].MakeCargo(central_sp
->produced_cargo
);
2843 row
.columns
[4].MakeEmpty(CFT_EMPTY
);
2845 /* Add central industry. */
2846 int central_row
= 1 + num_indrows
/ 2;
2847 this->fields
[central_row
].columns
[2].MakeIndustry(displayed_it
);
2848 this->fields
[central_row
].ConnectIndustryProduced(2);
2849 this->fields
[central_row
].ConnectIndustryAccepted(2);
2851 /* Add cargo labels. */
2852 this->fields
[central_row
- 1].MakeCargoLabel(2, true);
2853 this->fields
[central_row
+ 1].MakeCargoLabel(2, false);
2855 /* Add suppliers and customers of the 'it' industry. */
2858 for (IndustryType it
: _sorted_industry_types
) {
2859 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2860 if (!indsp
->enabled
) continue;
2862 if (HasCommonValidCargo(central_sp
->accepts_cargo
, indsp
->produced_cargo
)) {
2863 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, it
);
2864 _displayed_industries
.set(it
);
2867 if (HasCommonValidCargo(central_sp
->produced_cargo
, indsp
->accepts_cargo
)) {
2868 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 4, it
);
2869 _displayed_industries
.set(it
);
2873 if (houses_supply
) {
2874 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, NUM_INDUSTRYTYPES
);
2877 if (houses_accept
) {
2878 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 4, NUM_INDUSTRYTYPES
);
2882 this->ShortenCargoColumn(1, 1, num_indrows
);
2883 this->ShortenCargoColumn(3, 1, num_indrows
);
2884 this->vscroll
->SetCount(num_indrows
);
2886 this->NotifySmallmap();
2890 * Compute what and where to display for cargo id \a cid.
2891 * @param cid Cargo id to display.
2893 void ComputeCargoDisplay(CargoID cid
)
2895 this->GetWidget
<NWidgetCore
>(WID_IC_CAPTION
)->widget_data
= STR_INDUSTRY_CARGOES_CARGO_CAPTION
;
2896 this->ind_cargo
= cid
+ NUM_INDUSTRYTYPES
;
2897 _displayed_industries
.reset();
2899 this->fields
.clear();
2900 CargoesRow
&first_row
= this->fields
.emplace_back();
2901 first_row
.columns
[0].MakeHeader(STR_INDUSTRY_CARGOES_PRODUCERS
);
2902 first_row
.columns
[1].MakeEmpty(CFT_SMALL_EMPTY
);
2903 first_row
.columns
[2].MakeHeader(STR_INDUSTRY_CARGOES_CUSTOMERS
);
2904 first_row
.columns
[3].MakeEmpty(CFT_SMALL_EMPTY
);
2905 first_row
.columns
[4].MakeEmpty(CFT_SMALL_EMPTY
);
2907 auto cargoes
= std::span(&cid
, 1);
2908 bool houses_supply
= HousesCanSupply(cargoes
);
2909 bool houses_accept
= HousesCanAccept(cargoes
);
2910 int num_supp
= CountMatchingProducingIndustries(cargoes
) + houses_supply
+ 1; // Ensure room for the cargo label.
2911 int num_cust
= CountMatchingAcceptingIndustries(cargoes
) + houses_accept
;
2912 int num_indrows
= std::max(num_supp
, num_cust
);
2913 for (int i
= 0; i
< num_indrows
; i
++) {
2914 CargoesRow
&row
= this->fields
.emplace_back();
2915 row
.columns
[0].MakeEmpty(CFT_EMPTY
);
2916 row
.columns
[1].MakeCargo(cargoes
);
2917 row
.columns
[2].MakeEmpty(CFT_EMPTY
);
2918 row
.columns
[3].MakeEmpty(CFT_EMPTY
);
2919 row
.columns
[4].MakeEmpty(CFT_EMPTY
);
2922 this->fields
[num_indrows
].MakeCargoLabel(0, false); // Add cargo labels at the left bottom.
2924 /* Add suppliers and customers of the cargo. */
2927 for (IndustryType it
: _sorted_industry_types
) {
2928 const IndustrySpec
*indsp
= GetIndustrySpec(it
);
2929 if (!indsp
->enabled
) continue;
2931 if (HasCommonValidCargo(cargoes
, indsp
->produced_cargo
)) {
2932 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, it
);
2933 _displayed_industries
.set(it
);
2936 if (HasCommonValidCargo(cargoes
, indsp
->accepts_cargo
)) {
2937 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 2, it
);
2938 _displayed_industries
.set(it
);
2942 if (houses_supply
) {
2943 this->PlaceIndustry(1 + supp_count
* num_indrows
/ num_supp
, 0, NUM_INDUSTRYTYPES
);
2946 if (houses_accept
) {
2947 this->PlaceIndustry(1 + cust_count
* num_indrows
/ num_cust
, 2, NUM_INDUSTRYTYPES
);
2951 this->ShortenCargoColumn(1, 1, num_indrows
);
2952 this->vscroll
->SetCount(num_indrows
);
2954 this->NotifySmallmap();
2958 * Some data on this window has become invalid.
2959 * @param data Information about the changed data.
2960 * - data = 0 .. NUM_INDUSTRYTYPES - 1: Display the chain around the given industry.
2961 * - data = NUM_INDUSTRYTYPES: Stop sending updates to the smallmap window.
2962 * @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.
2964 void OnInvalidateData([[maybe_unused
]] int data
= 0, [[maybe_unused
]] bool gui_scope
= true) override
2966 if (!gui_scope
) return;
2967 if (data
== NUM_INDUSTRYTYPES
) {
2968 this->RaiseWidgetWhenLowered(WID_IC_NOTIFY
);
2972 assert(data
>= 0 && data
< NUM_INDUSTRYTYPES
);
2973 this->ComputeIndustryDisplay(data
);
2976 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
2978 if (widget
!= WID_IC_PANEL
) return;
2980 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.bevel
);
2981 DrawPixelInfo tmp_dpi
;
2982 if (!FillDrawPixelInfo(&tmp_dpi
, ir
)) return;
2983 AutoRestoreBackup
dpi_backup(_cur_dpi
, &tmp_dpi
);
2985 int left_pos
= WidgetDimensions::scaled
.frametext
.left
- WidgetDimensions::scaled
.bevel
.left
;
2986 if (this->ind_cargo
>= NUM_INDUSTRYTYPES
) left_pos
+= (CargoesField::industry_width
+ CargoesField::cargo_field_width
) / 2;
2987 int last_column
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 4 : 2;
2989 const NWidgetBase
*nwp
= this->GetWidget
<NWidgetBase
>(WID_IC_PANEL
);
2990 int vpos
= WidgetDimensions::scaled
.frametext
.top
- WidgetDimensions::scaled
.bevel
.top
- this->vscroll
->GetPosition() * nwp
->resize_y
;
2991 int row_height
= CargoesField::small_height
;
2992 for (const auto &field
: this->fields
) {
2993 if (vpos
+ row_height
>= 0) {
2994 int xpos
= left_pos
;
2996 if (_current_text_dir
== TD_RTL
) {
3003 while (col
>= 0 && col
<= last_column
) {
3004 field
.columns
[col
].Draw(xpos
, vpos
);
3005 xpos
+= (col
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
;
3010 if (vpos
>= height
) break;
3011 row_height
= CargoesField::normal_height
;
3016 * Calculate in which field was clicked, and within the field, at what position.
3017 * @param pt Clicked position in the #WID_IC_PANEL widget.
3018 * @param fieldxy If \c true is returned, field x/y coordinate of \a pt.
3019 * @param xy If \c true is returned, x/y coordinate with in the field.
3020 * @return Clicked at a valid position.
3022 bool CalculatePositionInWidget(Point pt
, Point
*fieldxy
, Point
*xy
)
3024 const NWidgetBase
*nw
= this->GetWidget
<NWidgetBase
>(WID_IC_PANEL
);
3028 int vpos
= WidgetDimensions::scaled
.frametext
.top
+ CargoesField::small_height
- this->vscroll
->GetPosition() * nw
->resize_y
;
3029 if (pt
.y
< vpos
) return false;
3031 int row
= (pt
.y
- vpos
) / CargoesField::normal_height
; // row is relative to row 1.
3032 if (row
+ 1 >= (int)this->fields
.size()) return false;
3033 vpos
= pt
.y
- vpos
- row
* CargoesField::normal_height
; // Position in the row + 1 field
3034 row
++; // rebase row to match index of this->fields.
3036 int xpos
= 2 * WidgetDimensions::scaled
.frametext
.left
+ ((this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 0 : (CargoesField::industry_width
+ CargoesField::cargo_field_width
) / 2);
3037 if (pt
.x
< xpos
) return false;
3039 for (column
= 0; column
<= 5; column
++) {
3040 int width
= (column
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
;
3041 if (pt
.x
< xpos
+ width
) break;
3044 int num_columns
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? 4 : 2;
3045 if (column
> num_columns
) return false;
3048 /* Return both positions, compensating for RTL languages (which works due to the equal symmetry in both displays). */
3051 if (_current_text_dir
== TD_RTL
) {
3052 fieldxy
->x
= num_columns
- column
;
3053 xy
->x
= ((column
& 1) ? CargoesField::cargo_field_width
: CargoesField::industry_width
) - xpos
;
3055 fieldxy
->x
= column
;
3061 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
3064 case WID_IC_PANEL
: {
3066 if (!CalculatePositionInWidget(pt
, &fieldxy
, &xy
)) return;
3068 const CargoesField
*fld
= this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
;
3069 switch (fld
->type
) {
3071 if (fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
) this->ComputeIndustryDisplay(fld
->u
.industry
.ind_type
);
3075 CargoesField
*lft
= (fieldxy
.x
> 0) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
- 1 : nullptr;
3076 CargoesField
*rgt
= (fieldxy
.x
< 4) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
+ 1 : nullptr;
3077 CargoID cid
= fld
->CargoClickedAt(lft
, rgt
, xy
);
3078 if (IsValidCargoID(cid
)) this->ComputeCargoDisplay(cid
);
3082 case CFT_CARGO_LABEL
: {
3083 CargoID cid
= fld
->CargoLabelClickedAt(xy
);
3084 if (IsValidCargoID(cid
)) this->ComputeCargoDisplay(cid
);
3095 this->ToggleWidgetLoweredState(WID_IC_NOTIFY
);
3096 this->SetWidgetDirty(WID_IC_NOTIFY
);
3097 if (_settings_client
.sound
.click_beep
) SndPlayFx(SND_15_BEEP
);
3099 if (this->IsWidgetLowered(WID_IC_NOTIFY
)) {
3100 if (FindWindowByClass(WC_SMALLMAP
) == nullptr) ShowSmallMap();
3101 this->NotifySmallmap();
3105 case WID_IC_CARGO_DROPDOWN
: {
3107 Dimension d
= GetLargestCargoIconSize();
3108 for (const CargoSpec
*cs
: _sorted_standard_cargo_specs
) {
3109 lst
.push_back(MakeDropDownListIconItem(d
, cs
->GetCargoIcon(), PAL_NONE
, cs
->name
, cs
->Index()));
3112 int selected
= (this->ind_cargo
>= NUM_INDUSTRYTYPES
) ? (int)(this->ind_cargo
- NUM_INDUSTRYTYPES
) : -1;
3113 ShowDropDownList(this, std::move(lst
), selected
, WID_IC_CARGO_DROPDOWN
);
3118 case WID_IC_IND_DROPDOWN
: {
3120 for (IndustryType ind
: _sorted_industry_types
) {
3121 const IndustrySpec
*indsp
= GetIndustrySpec(ind
);
3122 if (!indsp
->enabled
) continue;
3123 lst
.push_back(MakeDropDownListStringItem(indsp
->name
, ind
));
3126 int selected
= (this->ind_cargo
< NUM_INDUSTRYTYPES
) ? (int)this->ind_cargo
: -1;
3127 ShowDropDownList(this, std::move(lst
), selected
, WID_IC_IND_DROPDOWN
);
3134 void OnDropdownSelect(WidgetID widget
, int index
) override
3136 if (index
< 0) return;
3139 case WID_IC_CARGO_DROPDOWN
:
3140 this->ComputeCargoDisplay(index
);
3143 case WID_IC_IND_DROPDOWN
:
3144 this->ComputeIndustryDisplay(index
);
3149 bool OnTooltip([[maybe_unused
]] Point pt
, WidgetID widget
, TooltipCloseCondition close_cond
) override
3151 if (widget
!= WID_IC_PANEL
) return false;
3154 if (!CalculatePositionInWidget(pt
, &fieldxy
, &xy
)) return false;
3156 const CargoesField
*fld
= this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
;
3157 CargoID cid
= INVALID_CARGO
;
3158 switch (fld
->type
) {
3160 CargoesField
*lft
= (fieldxy
.x
> 0) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
- 1 : nullptr;
3161 CargoesField
*rgt
= (fieldxy
.x
< 4) ? this->fields
[fieldxy
.y
].columns
+ fieldxy
.x
+ 1 : nullptr;
3162 cid
= fld
->CargoClickedAt(lft
, rgt
, xy
);
3166 case CFT_CARGO_LABEL
: {
3167 cid
= fld
->CargoLabelClickedAt(xy
);
3172 if (fld
->u
.industry
.ind_type
< NUM_INDUSTRYTYPES
&& (this->ind_cargo
>= NUM_INDUSTRYTYPES
|| fieldxy
.x
!= 2)) {
3173 GuiShowTooltips(this, STR_INDUSTRY_CARGOES_INDUSTRY_TOOLTIP
, close_cond
);
3180 if (IsValidCargoID(cid
) && (this->ind_cargo
< NUM_INDUSTRYTYPES
|| cid
!= this->ind_cargo
- NUM_INDUSTRYTYPES
)) {
3181 const CargoSpec
*csp
= CargoSpec::Get(cid
);
3182 SetDParam(0, csp
->name
);
3183 GuiShowTooltips(this, STR_INDUSTRY_CARGOES_CARGO_TOOLTIP
, close_cond
, 1);
3190 void OnResize() override
3192 this->vscroll
->SetCapacityFromWidget(this, WID_IC_PANEL
, WidgetDimensions::scaled
.framerect
.Vertical() + CargoesField::small_height
);
3197 * Open the industry and cargoes window.
3198 * @param id Industry type to display, \c NUM_INDUSTRYTYPES selects a default industry type.
3200 static void ShowIndustryCargoesWindow(IndustryType id
)
3202 if (id
>= NUM_INDUSTRYTYPES
) {
3203 for (IndustryType ind
: _sorted_industry_types
) {
3204 const IndustrySpec
*indsp
= GetIndustrySpec(ind
);
3205 if (indsp
->enabled
) {
3210 if (id
>= NUM_INDUSTRYTYPES
) return;
3213 Window
*w
= BringWindowToFrontById(WC_INDUSTRY_CARGOES
, 0);
3215 w
->InvalidateData(id
);
3218 new IndustryCargoesWindow(id
);
3221 /** Open the industry and cargoes window with an industry. */
3222 void ShowIndustryCargoesWindow()
3224 ShowIndustryCargoesWindow(NUM_INDUSTRYTYPES
);