Fix: Don't allow right-click to close world generation progress window. (#13084)
[openttd-github.git] / src / object_gui.cpp
blob8bc2e98d4c6f1851ed8d05397bc4a7c84bc99949
1 /*
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/>.
6 */
8 /** @file object_gui.cpp The GUI for objects. */
10 #include "stdafx.h"
11 #include "command_func.h"
12 #include "company_func.h"
13 #include "hotkeys.h"
14 #include "newgrf.h"
15 #include "newgrf_object.h"
16 #include "newgrf_text.h"
17 #include "object.h"
18 #include "object_base.h"
19 #include "picker_gui.h"
20 #include "sound_func.h"
21 #include "strings_func.h"
22 #include "viewport_func.h"
23 #include "tilehighlight_func.h"
24 #include "window_gui.h"
25 #include "window_func.h"
26 #include "zoom_func.h"
27 #include "terraform_cmd.h"
28 #include "object_cmd.h"
29 #include "road_cmd.h"
31 #include "widgets/object_widget.h"
33 #include "table/strings.h"
35 #include "safeguards.h"
37 struct ObjectPickerSelection {
38 ObjectClassID sel_class; ///< Selected object class.
39 uint16_t sel_type; ///< Selected object type within the class.
40 uint8_t sel_view; ///< Selected view of the object.
42 static ObjectPickerSelection _object_gui; ///< Settings of the object picker.
44 class ObjectPickerCallbacks : public PickerCallbacksNewGRFClass<ObjectClass> {
45 public:
46 ObjectPickerCallbacks() : PickerCallbacksNewGRFClass<ObjectClass>("fav_objects") {}
48 StringID GetClassTooltip() const override { return STR_PICKER_OBJECT_CLASS_TOOLTIP; }
49 StringID GetTypeTooltip() const override { return STR_PICKER_OBJECT_TYPE_TOOLTIP; }
51 bool IsActive() const override
53 for (const auto &cls : ObjectClass::Classes()) {
54 for (const auto *spec : cls.Specs()) {
55 if (spec != nullptr && spec->IsEverAvailable()) return true;
58 return false;
61 int GetSelectedClass() const override { return _object_gui.sel_class; }
62 void SetSelectedClass(int id) const override { _object_gui.sel_class = this->GetClassIndex(id); }
64 StringID GetClassName(int id) const override
66 const auto *objclass = this->GetClass(id);
67 if (objclass->GetUISpecCount() == 0) return INVALID_STRING_ID;
68 return objclass->name;
71 int GetSelectedType() const override { return _object_gui.sel_type; }
72 void SetSelectedType(int id) const override { _object_gui.sel_type = id; }
74 StringID GetTypeName(int cls_id, int id) const override
76 const auto *spec = this->GetSpec(cls_id, id);
77 return (spec == nullptr || !spec->IsEverAvailable()) ? INVALID_STRING_ID : spec->name;
80 bool IsTypeAvailable(int cls_id, int id) const override
82 const auto *spec = this->GetSpec(cls_id, id);
83 return spec->IsAvailable();
86 void DrawType(int x, int y, int cls_id, int id) const override
88 const auto *spec = this->GetSpec(cls_id, id);
89 if (spec->grf_prop.grffile == nullptr) {
90 extern const DrawTileSprites _objects[];
91 const DrawTileSprites *dts = &_objects[spec->grf_prop.local_id];
92 DrawOrigTileSeqInGUI(x, y, dts, PAL_NONE);
93 } else {
94 DrawNewObjectTileInGUI(x, y, spec, std::min<int>(_object_gui.sel_view, spec->views - 1));
98 void FillUsedItems(std::set<PickerItem> &items) override
100 for (const Object *o : Object::Iterate()) {
101 if (GetTileOwner(o->location.tile) != _current_company) continue;
102 const ObjectSpec *spec = ObjectSpec::Get(o->type);
103 if (spec == nullptr || spec->class_index == INVALID_OBJECT_CLASS || !spec->IsEverAvailable()) continue;
104 items.insert(GetPickerItem(spec));
108 static ObjectPickerCallbacks instance;
110 /* static */ ObjectPickerCallbacks ObjectPickerCallbacks::instance;
112 /** The window used for building objects. */
113 class BuildObjectWindow : public PickerWindow {
114 int info_height; ///< The height of the info box.
116 public:
117 BuildObjectWindow(WindowDesc &desc, WindowNumber) : PickerWindow(desc, nullptr, 0, ObjectPickerCallbacks::instance), info_height(1)
119 ResetObjectToPlace();
120 this->ConstructWindow();
121 this->InvalidateData();
124 void SetStringParameters(WidgetID widget) const override
126 switch (widget) {
127 case WID_BO_OBJECT_SIZE: {
128 ObjectClass *objclass = ObjectClass::Get(_object_gui.sel_class);
129 const ObjectSpec *spec = objclass->GetSpec(_object_gui.sel_type);
130 int size = spec == nullptr ? 0 : spec->size;
131 SetDParam(0, GB(size, HasBit(_object_gui.sel_view, 0) ? 4 : 0, 4));
132 SetDParam(1, GB(size, HasBit(_object_gui.sel_view, 0) ? 0 : 4, 4));
133 break;
136 default:
137 this->PickerWindow::SetStringParameters(widget);
138 break;
142 void OnInit() override
144 this->GetWidget<NWidgetMatrix>(WID_BO_OBJECT_MATRIX)->SetCount(4);
145 this->PickerWindow::OnInit();
148 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
150 switch (widget) {
151 case WID_BO_OBJECT_SIZE:
152 /* We do not want the window to resize when selecting objects; better clip texts */
153 size.width = 0;
154 break;
156 case WID_BO_OBJECT_MATRIX: {
157 /* Get the right amount of buttons based on the current spec. */
158 const ObjectClass *objclass = ObjectClass::Get(_object_gui.sel_class);
159 const ObjectSpec *spec = objclass->GetSpec(_object_gui.sel_type);
160 if (spec != nullptr) {
161 if (spec->views >= 2) size.width += resize.width;
162 if (spec->views >= 4) size.height += resize.height;
164 resize.width = 0;
165 resize.height = 0;
166 break;
169 case WID_BO_OBJECT_SPRITE: {
170 /* Get the right amount of buttons based on the current spec. */
171 const ObjectClass *objclass = ObjectClass::Get(_object_gui.sel_class);
172 const ObjectSpec *spec = objclass->GetSpec(_object_gui.sel_type);
173 size.width = ScaleGUITrad(PREVIEW_WIDTH) + WidgetDimensions::scaled.fullbevel.Horizontal();
174 size.height = ScaleGUITrad(PREVIEW_HEIGHT) + WidgetDimensions::scaled.fullbevel.Vertical();
175 if (spec != nullptr) {
176 if (spec->views <= 1) size.width = size.width * 2 + WidgetDimensions::scaled.hsep_normal;
177 if (spec->views <= 2) size.height = size.height * 2 + WidgetDimensions::scaled.vsep_normal;
179 break;
182 case WID_BO_INFO:
183 size.height = this->info_height;
184 break;
186 default:
187 this->PickerWindow::UpdateWidgetSize(widget, size, padding, fill, resize);
188 break;
192 void DrawWidget(const Rect &r, WidgetID widget) const override
194 switch (widget) {
195 case WID_BO_OBJECT_SPRITE: {
196 const ObjectClass *objclass = ObjectClass::Get(_object_gui.sel_class);
197 const ObjectSpec *spec = objclass->GetSpec(_object_gui.sel_type);
198 if (spec == nullptr) break;
200 const NWidgetMatrix *matrix = this->GetWidget<NWidgetBase>(widget)->GetParentWidget<NWidgetMatrix>();
202 DrawPixelInfo tmp_dpi;
203 /* Set up a clipping area for the preview. */
204 Rect ir = r.Shrink(WidgetDimensions::scaled.bevel);
205 if (FillDrawPixelInfo(&tmp_dpi, ir)) {
206 AutoRestoreBackup dpi_backup(_cur_dpi, &tmp_dpi);
207 int x = (ir.Width() - ScaleSpriteTrad(PREVIEW_WIDTH)) / 2 + ScaleSpriteTrad(PREVIEW_LEFT);
208 int y = (ir.Height() + ScaleSpriteTrad(PREVIEW_HEIGHT)) / 2 - ScaleSpriteTrad(PREVIEW_BOTTOM);
210 if (spec->grf_prop.grffile == nullptr) {
211 extern const DrawTileSprites _objects[];
212 const DrawTileSprites *dts = &_objects[spec->grf_prop.local_id];
213 DrawOrigTileSeqInGUI(x, y, dts, PAL_NONE);
214 } else {
215 DrawNewObjectTileInGUI(x, y, spec, matrix->GetCurrentElement());
218 break;
221 case WID_BO_INFO: {
222 const ObjectClass *objclass = ObjectClass::Get(_object_gui.sel_class);
223 const ObjectSpec *spec = objclass->GetSpec(_object_gui.sel_type);
224 if (spec == nullptr) break;
226 /* Get the extra message for the GUI */
227 if (HasBit(spec->callback_mask, CBM_OBJ_FUND_MORE_TEXT)) {
228 uint16_t callback_res = GetObjectCallback(CBID_OBJECT_FUND_MORE_TEXT, 0, 0, spec, nullptr, INVALID_TILE, _object_gui.sel_view);
229 if (callback_res != CALLBACK_FAILED && callback_res != 0x400) {
230 if (callback_res > 0x400) {
231 ErrorUnknownCallbackResult(spec->grf_prop.grffile->grfid, CBID_OBJECT_FUND_MORE_TEXT, callback_res);
232 } else {
233 StringID message = GetGRFStringID(spec->grf_prop.grffile->grfid, 0xD000 + callback_res);
234 if (message != STR_NULL && message != STR_UNDEFINED) {
235 StartTextRefStackUsage(spec->grf_prop.grffile, 6);
236 /* Use all the available space left from where we stand up to the
237 * end of the window. We ALSO enlarge the window if needed, so we
238 * can 'go' wild with the bottom of the window. */
239 int y = DrawStringMultiLine(r.left, r.right, r.top, UINT16_MAX, message, TC_ORANGE) - r.top - 1;
240 StopTextRefStackUsage();
241 if (y > this->info_height) {
242 BuildObjectWindow *bow = const_cast<BuildObjectWindow *>(this);
243 bow->info_height = y;
244 bow->ReInit();
250 break;
253 default:
254 this->PickerWindow::DrawWidget(r, widget);
255 break;
259 void UpdateSelectSize(const ObjectSpec *spec)
261 if (spec == nullptr) {
262 SetTileSelectSize(1, 1);
263 ResetObjectToPlace();
264 } else {
265 _object_gui.sel_view = std::min<int>(_object_gui.sel_view, spec->views - 1);
266 SetObjectToPlaceWnd(SPR_CURSOR_TRANSMITTER, PAL_NONE, HT_RECT | HT_DIAGONAL, this);
267 int w = GB(spec->size, HasBit(_object_gui.sel_view, 0) ? 4 : 0, 4);
268 int h = GB(spec->size, HasBit(_object_gui.sel_view, 0) ? 0 : 4, 4);
269 SetTileSelectSize(w, h);
270 this->ReInit();
275 * Update buttons to show the selection to the user.
276 * @param spec The object spec of the selected object.
278 void UpdateButtons(const ObjectSpec *spec)
280 this->GetWidget<NWidgetMatrix>(WID_BO_OBJECT_MATRIX)->SetClicked(_object_gui.sel_view);
281 this->UpdateSelectSize(spec);
282 this->SetDirty();
285 void OnInvalidateData([[maybe_unused]] int data = 0, [[maybe_unused]] bool gui_scope = true) override
287 this->PickerWindow::OnInvalidateData(data, gui_scope);
289 if (!gui_scope) return;
291 if ((data & PickerWindow::PFI_POSITION) != 0) {
292 const auto objclass = ObjectClass::Get(_object_gui.sel_class);
293 const auto spec = objclass->GetSpec(_object_gui.sel_type);
294 _object_gui.sel_view = std::min<int>(_object_gui.sel_view, spec->views - 1);
295 this->UpdateButtons(spec);
299 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
301 switch (widget) {
302 case WID_BO_OBJECT_SPRITE:
303 if (_object_gui.sel_type != MAX_UVALUE(uint16_t)) {
304 _object_gui.sel_view = this->GetWidget<NWidgetBase>(widget)->GetParentWidget<NWidgetMatrix>()->GetCurrentElement();
305 this->InvalidateData(PickerWindow::PFI_POSITION);
306 if (_settings_client.sound.click_beep) SndPlayFx(SND_15_BEEP);
308 break;
310 default:
311 this->PickerWindow::OnClick(pt, widget, click_count);
312 break;
316 void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override
318 const ObjectSpec *spec = ObjectClass::Get(_object_gui.sel_class)->GetSpec(_object_gui.sel_type);
320 if (spec->size == OBJECT_SIZE_1X1) {
321 VpStartPlaceSizing(tile, VPM_X_AND_Y, DDSP_BUILD_OBJECT);
322 } else {
323 Command<CMD_BUILD_OBJECT>::Post(STR_ERROR_CAN_T_BUILD_OBJECT, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index(), _object_gui.sel_view);
327 void OnPlaceDrag(ViewportPlaceMethod select_method, [[maybe_unused]] ViewportDragDropSelectionProcess select_proc, [[maybe_unused]] Point pt) override
329 VpSelectTilesWithMethod(pt.x, pt.y, select_method);
332 void OnPlaceMouseUp([[maybe_unused]] ViewportPlaceMethod select_method, [[maybe_unused]] ViewportDragDropSelectionProcess select_proc, [[maybe_unused]] Point pt, TileIndex start_tile, TileIndex end_tile) override
334 if (pt.x == -1) return;
336 assert(select_proc == DDSP_BUILD_OBJECT);
338 if (!_settings_game.construction.freeform_edges) {
339 /* When end_tile is MP_VOID, the error tile will not be visible to the
340 * user. This happens when terraforming at the southern border. */
341 if (TileX(end_tile) == Map::MaxX()) end_tile += TileDiffXY(-1, 0);
342 if (TileY(end_tile) == Map::MaxY()) end_tile += TileDiffXY(0, -1);
344 const ObjectSpec *spec = ObjectClass::Get(_object_gui.sel_class)->GetSpec(_object_gui.sel_type);
345 Command<CMD_BUILD_OBJECT_AREA>::Post(STR_ERROR_CAN_T_BUILD_OBJECT, CcPlaySound_CONSTRUCTION_OTHER,
346 end_tile, start_tile, spec->Index(), _object_gui.sel_view, (_ctrl_pressed ? true : false));
349 void OnPlaceObjectAbort() override
351 this->UpdateButtons(nullptr);
355 * Handler for global hotkeys of the BuildObjectWindow.
356 * @param hotkey Hotkey
357 * @return ES_HANDLED if hotkey was accepted.
359 static EventState BuildObjectGlobalHotkeys(int hotkey)
361 if (_game_mode == GM_MENU) return ES_NOT_HANDLED;
362 Window *w = ShowBuildObjectPicker();
363 if (w == nullptr) return ES_NOT_HANDLED;
364 return w->OnHotkey(hotkey);
367 static inline HotkeyList hotkeys{"buildobject", {
368 Hotkey('F', "focus_filter_box", PCWHK_FOCUS_FILTER_BOX),
369 }, BuildObjectGlobalHotkeys};
372 static constexpr NWidgetPart _nested_build_object_widgets[] = {
373 NWidget(NWID_HORIZONTAL),
374 NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN),
375 NWidget(WWT_CAPTION, COLOUR_DARK_GREEN), SetDataTip(STR_OBJECT_BUILD_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
376 NWidget(WWT_SHADEBOX, COLOUR_DARK_GREEN),
377 NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN),
378 NWidget(WWT_STICKYBOX, COLOUR_DARK_GREEN),
379 EndContainer(),
380 NWidget(NWID_HORIZONTAL),
381 NWidget(NWID_VERTICAL),
382 NWidgetFunction(MakePickerClassWidgets),
383 NWidget(WWT_PANEL, COLOUR_DARK_GREEN),
384 NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_picker, 0), SetPadding(WidgetDimensions::unscaled.picker),
385 NWidget(WWT_LABEL, COLOUR_DARK_GREEN), SetDataTip(STR_STATION_BUILD_ORIENTATION, STR_NULL), SetFill(1, 0),
386 NWidget(NWID_HORIZONTAL), SetPIPRatio(1, 0, 1),
387 NWidget(NWID_MATRIX, COLOUR_DARK_GREEN, WID_BO_OBJECT_MATRIX), SetPIP(0, 2, 0),
388 NWidget(WWT_PANEL, COLOUR_GREY, WID_BO_OBJECT_SPRITE), SetDataTip(0x0, STR_OBJECT_BUILD_PREVIEW_TOOLTIP), EndContainer(),
389 EndContainer(),
390 EndContainer(),
391 NWidget(WWT_TEXT, COLOUR_DARK_GREEN, WID_BO_OBJECT_SIZE), SetDataTip(STR_OBJECT_BUILD_SIZE, STR_NULL), SetAlignment(SA_CENTER),
392 NWidget(WWT_EMPTY, INVALID_COLOUR, WID_BO_INFO), SetFill(1, 0), SetResize(1, 0),
393 EndContainer(),
394 EndContainer(),
395 EndContainer(),
396 NWidgetFunction(MakePickerTypeWidgets),
397 EndContainer(),
400 static WindowDesc _build_object_desc(
401 WDP_AUTO, "build_object", 0, 0,
402 WC_BUILD_OBJECT, WC_BUILD_TOOLBAR,
403 WDF_CONSTRUCTION,
404 _nested_build_object_widgets,
405 &BuildObjectWindow::hotkeys
408 /** Show our object picker. */
409 Window *ShowBuildObjectPicker()
411 /* Don't show the place object button when there are no objects to place. */
412 if (ObjectPickerCallbacks::instance.IsActive()) {
413 return AllocateWindowDescFront<BuildObjectWindow>(_build_object_desc, 0);
415 return nullptr;
418 /** Reset all data of the object GUI. */
419 void InitializeObjectGui()
421 _object_gui.sel_class = ObjectClassID::OBJECT_CLASS_BEGIN;