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 object_gui.cpp The GUI for objects. */
11 #include "command_func.h"
12 #include "company_func.h"
15 #include "newgrf_object.h"
16 #include "newgrf_text.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"
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
> {
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;
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
);
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.
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
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));
137 this->PickerWindow::SetStringParameters(widget
);
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
151 case WID_BO_OBJECT_SIZE
:
152 /* We do not want the window to resize when selecting objects; better clip texts */
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
;
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
;
183 size
.height
= this->info_height
;
187 this->PickerWindow::UpdateWidgetSize(widget
, size
, padding
, fill
, resize
);
192 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
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
);
215 DrawNewObjectTileInGUI(x
, y
, spec
, matrix
->GetCurrentElement());
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
);
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
;
254 this->PickerWindow::DrawWidget(r
, widget
);
259 void UpdateSelectSize(const ObjectSpec
*spec
)
261 if (spec
== nullptr) {
262 SetTileSelectSize(1, 1);
263 ResetObjectToPlace();
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
);
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
);
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
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
);
311 this->PickerWindow::OnClick(pt
, widget
, click_count
);
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
);
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
),
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(),
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),
396 NWidgetFunction(MakePickerTypeWidgets
),
400 static WindowDesc
_build_object_desc(
401 WDP_AUTO
, "build_object", 0, 0,
402 WC_BUILD_OBJECT
, WC_BUILD_TOOLBAR
,
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);
418 /** Reset all data of the object GUI. */
419 void InitializeObjectGui()
421 _object_gui
.sel_class
= ObjectClassID::OBJECT_CLASS_BEGIN
;