1#include "menus_ingame_touch_controls.h"
2
3#include <base/color.h>
4#include <base/dbg.h>
5#include <base/math.h>
6#include <base/str.h>
7
8#include <engine/external/json-parser/json.h>
9#include <engine/font_icons.h>
10#include <engine/graphics.h>
11#include <engine/shared/jsonwriter.h>
12#include <engine/shared/localization.h>
13#include <engine/textrender.h>
14
15#include <game/client/components/touch_controls.h>
16#include <game/client/gameclient.h>
17#include <game/client/lineinput.h>
18#include <game/client/ui.h>
19#include <game/client/ui_listbox.h>
20#include <game/client/ui_rect.h>
21#include <game/client/ui_scrollregion.h>
22#include <game/localization.h>
23
24#include <algorithm>
25#include <memory>
26#include <string>
27
28static const constexpr float MAINMARGIN = 10.0f;
29static const constexpr float SUBMARGIN = 5.0f;
30static const constexpr float ROWSIZE = 25.0f;
31static const constexpr float ROWGAP = 5.0f;
32static const constexpr float TITLESIZE = 20.0f;
33static const constexpr float FONTSIZE = 15.0f;
34
35const CMenusIngameTouchControls::CBehaviorFactoryEditor CMenusIngameTouchControls::BEHAVIOR_FACTORIES_EDITOR[] = {
36 {.m_pId: CTouchControls::CIngameMenuTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CIngameMenuTouchButtonBehavior>(); }},
37 {.m_pId: CTouchControls::CExtraMenuTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CExtraMenuTouchButtonBehavior>(args: 0); }},
38 {.m_pId: CTouchControls::CEmoticonTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CEmoticonTouchButtonBehavior>(); }},
39 {.m_pId: CTouchControls::CSpectateTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CSpectateTouchButtonBehavior>(); }},
40 {.m_pId: CTouchControls::CSwapActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CSwapActionTouchButtonBehavior>(); }},
41 {.m_pId: CTouchControls::CUseActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CUseActionTouchButtonBehavior>(); }},
42 {.m_pId: CTouchControls::CJoystickActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickActionTouchButtonBehavior>(); }},
43 {.m_pId: CTouchControls::CJoystickAimTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickAimTouchButtonBehavior>(); }},
44 {.m_pId: CTouchControls::CJoystickFireTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickFireTouchButtonBehavior>(); }},
45 {.m_pId: CTouchControls::CJoystickHookTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickHookTouchButtonBehavior>(); }}};
46
47void CMenusIngameTouchControls::RenderTouchButtonEditor(CUIRect MainView)
48{
49 if(!GameClient()->m_TouchControls.IsButtonEditing())
50 {
51 RenderTouchButtonBrowser(MainView);
52 return;
53 }
54 // Used to decide if need to update the Samplebutton.
55 bool Changed = false;
56 CUIRect Functional, LeftButton, MiddleButton, RightButton, EditBox, Block;
57 MainView.h = 600.0f - 40.0f - MainView.y;
58 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
59 MainView.VMargin(Cut: MAINMARGIN, pOtherRect: &MainView);
60 MainView.HSplitTop(Cut: MAINMARGIN, pTop: nullptr, pBottom: &MainView);
61 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
62
63 MainView.HSplitBottom(Cut: 2 * ROWSIZE + 2 * ROWGAP + MAINMARGIN, pTop: &Block, pBottom: &Functional);
64 Functional.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Functional);
65 Functional.HSplitBottom(Cut: MAINMARGIN, pTop: &Functional, pBottom: nullptr);
66
67 // Choosing which to edit.
68 EditBox.VSplitLeft(Cut: EditBox.w / 3.0f, pLeft: &RightButton, pRight: &EditBox);
69 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
70
71 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: m_aEditElementIds.data(), pText: Localize(pStr: "Layout"), Checked: m_EditElement == EElementType::LAYOUT, pRect: &RightButton, Corners: IGraphics::CORNER_TL, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 5.0f))
72 {
73 m_EditElement = EElementType::LAYOUT;
74 }
75 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &m_aEditElementIds[1], pText: Localize(pStr: "Visibility"), Checked: m_EditElement == EElementType::VISIBILITY, pRect: &LeftButton, Corners: IGraphics::CORNER_NONE, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 5.0f))
76 {
77 m_EditElement = EElementType::VISIBILITY;
78 }
79 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &m_aEditElementIds[2], pText: Localize(pStr: "Behavior"), Checked: m_EditElement == EElementType::BEHAVIOR, pRect: &MiddleButton, Corners: IGraphics::CORNER_TR, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 5.0f))
80 {
81 m_EditElement = EElementType::BEHAVIOR;
82 }
83
84 // Edit blocks.
85 Block.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
86 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
87 Block.VMargin(Cut: SUBMARGIN, pOtherRect: &Block);
88 switch(m_EditElement)
89 {
90 case EElementType::LAYOUT: Changed |= RenderLayoutSettingBlock(Block); break;
91 case EElementType::VISIBILITY: Changed |= RenderVisibilitySettingBlock(Block); break;
92 case EElementType::BEHAVIOR: Changed |= RenderBehaviorSettingBlock(Block); break;
93 default: dbg_assert_failed("Unknown m_EditElement = %d.", (int)m_EditElement);
94 }
95
96 // Save & Cancel & Hint.
97 Functional.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Functional);
98 const float ButtonWidth = (EditBox.w - SUBMARGIN * 2.0f) / 3.0f;
99 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &LeftButton, pRight: &EditBox);
100 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
101 static CButtonContainer s_ConfirmButton;
102 // After touching this button, the button is then added into the button vector. Or it is still virtual.
103 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ConfirmButton, pText: Localize(pStr: "Save changes"), Checked: UnsavedChanges() ? 0 : 1, pRect: &LeftButton))
104 {
105 if(UnsavedChanges())
106 {
107 m_pOldSelectedButton = GameClient()->m_TouchControls.SelectedButton();
108 if(CheckCachedSettings())
109 {
110 SaveCachedSettingsToTarget(pTargetButton: m_pOldSelectedButton);
111 GameClient()->m_TouchControls.SetSelectedButton(m_pOldSelectedButton);
112 GameClient()->m_TouchControls.SetEditingChanges(true);
113 SetUnsavedChanges(false);
114 }
115 }
116 }
117 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &LeftButton, pRight: &EditBox);
118 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &MiddleButton);
119 if(UnsavedChanges())
120 {
121 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
122 Ui()->DoLabel(pRect: &LeftButton, pText: Localize(pStr: "Unsaved changes"), Size: 14.0f, Align: TEXTALIGN_MC);
123 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
124 }
125
126 static CButtonContainer s_CancelButton;
127 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_CancelButton, pText: Localize(pStr: "Discard changes"), Checked: UnsavedChanges() ? 0 : 1, pRect: &MiddleButton))
128 {
129 // Since the settings are canceled, reset the cached settings to m_pSelectedButton though selected button didn't change.
130 // Reset changes to default if the button is still virtual.
131 if(UnsavedChanges())
132 {
133 CacheAllSettingsFromTarget(pTargetButton: GameClient()->m_TouchControls.SelectedButton());
134 Changed = true;
135 SetUnsavedChanges(false);
136 }
137 // Cancel does nothing if nothing is unsaved.
138 }
139
140 // Functional Buttons.
141 Functional.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Functional);
142 Functional.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Functional);
143 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &LeftButton, pRight: &EditBox);
144 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
145 static CButtonContainer s_RemoveButton;
146 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_RemoveButton, pText: Localize(pStr: "Delete"), Checked: 0, pRect: &LeftButton))
147 {
148 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Delete button"), pMessage: Localize(pStr: "Are you sure that you want to delete this button?"), pConfirmButtonLabel: Localize(pStr: "Delete"), pCancelButtonLabel: Localize(pStr: "Cancel"), pfnConfirmButtonCallback: &CMenus::PopupConfirmDeleteButton);
149 }
150
151 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &LeftButton, pRight: &EditBox);
152 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &MiddleButton);
153 // Create a new button with current cached settings. New button will be automatically moved to nearest empty space.
154 static CButtonContainer s_DuplicateButton;
155 bool Checked = UnsavedChanges();
156 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_DuplicateButton, pText: Localize(pStr: "Duplicate"), Checked: UnsavedChanges() || Checked ? 1 : 0, pRect: &LeftButton))
157 {
158 if(Checked)
159 {
160 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Unsaved changes"), pMessage: Localize(pStr: "Please save your changes before duplicating a button."), pButtonLabel: Localize(pStr: "Ok"));
161 }
162 else if(NewButton(Rect: GameClient()->m_TouchControls.ShownRect().value(), Shape: m_CachedShape))
163 {
164 m_pNewSelectedButton = GameClient()->m_TouchControls.SelectedButton();
165 SaveCachedSettingsToTarget(pTargetButton: m_pNewSelectedButton);
166 UpdateSampleButton();
167 GameClient()->m_TouchControls.SetEditingChanges(true);
168 }
169 }
170
171 // Deselect a button.
172 static CButtonContainer s_DeselectButton;
173 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_DeselectButton, pText: Localize(pStr: "Deselect"), Checked: 0, pRect: &MiddleButton))
174 {
175 m_pOldSelectedButton = GameClient()->m_TouchControls.SelectedButton();
176 m_pNewSelectedButton = nullptr;
177 if(UnsavedChanges())
178 {
179 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Unsaved changes"), pMessage: Localize(pStr: "You'll lose unsaved changes after deselecting."), pConfirmButtonLabel: Localize(pStr: "Deselect"), pCancelButtonLabel: Localize(pStr: "Cancel"), pfnConfirmButtonCallback: &CMenus::PopupCancelDeselectButton);
180 }
181 else
182 {
183 GameClient()->m_Menus.PopupCancelDeselectButton();
184 }
185 }
186
187 // This ensures m_pSampleButton being updated always.
188 if(Changed)
189 {
190 UpdateSampleButton();
191 }
192}
193
194bool CMenusIngameTouchControls::RenderLayoutSettingBlock(CUIRect Block)
195{
196 bool Changed = false;
197 CUIRect EditBox, LeftButton, RightButton, PosX, PosY, PosW, PosH;
198 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
199 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
200 EditBox.VSplitMid(pLeft: &PosX, pRight: &EditBox);
201 if(Ui()->DoClearableEditBox(pLineInput: &m_InputX, pRect: &EditBox, FontSize: FONTSIZE))
202 {
203 InputPosFunction(pInput: &m_InputX);
204 Changed = true;
205 }
206
207 // Auto check if the input value contains char that is not digit. If so delete it.
208 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
209 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
210 EditBox.VSplitMid(pLeft: &PosY, pRight: &EditBox);
211 if(Ui()->DoClearableEditBox(pLineInput: &m_InputY, pRect: &EditBox, FontSize: FONTSIZE))
212 {
213 InputPosFunction(pInput: &m_InputY);
214 Changed = true;
215 }
216
217 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
218 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
219 EditBox.VSplitMid(pLeft: &PosW, pRight: &EditBox);
220 if(Ui()->DoClearableEditBox(pLineInput: &m_InputW, pRect: &EditBox, FontSize: FONTSIZE))
221 {
222 InputPosFunction(pInput: &m_InputW);
223 Changed = true;
224 }
225
226 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
227 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
228 EditBox.VSplitMid(pLeft: &PosH, pRight: &EditBox);
229 if(Ui()->DoClearableEditBox(pLineInput: &m_InputH, pRect: &EditBox, FontSize: FONTSIZE))
230 {
231 InputPosFunction(pInput: &m_InputH);
232 Changed = true;
233 }
234 auto [X, Y, W, H] = GetPosInputs();
235 auto DoValidatedLabel = [&](const CUIRect &LabelBlock, const char *pLabel, int Size, bool Valid) {
236 if(pLabel == nullptr)
237 return;
238 if(!Valid)
239 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
240 Ui()->DoLabel(pRect: &LabelBlock, pText: pLabel, Size, Align: TEXTALIGN_ML);
241 if(!Valid)
242 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
243 };
244
245 DoValidatedLabel(PosX, "X:", FONTSIZE, 0 <= X && X + W <= CTouchControls::BUTTON_SIZE_SCALE);
246 DoValidatedLabel(PosY, "Y:", FONTSIZE, 0 <= Y && Y + H <= CTouchControls::BUTTON_SIZE_SCALE);
247 // There're strings without ":" below, so don't localize these with ":" together.
248 char aBuf[64];
249 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Width"));
250 DoValidatedLabel(PosW, aBuf, FONTSIZE, CTouchControls::BUTTON_SIZE_MINIMUM <= W && W <= CTouchControls::BUTTON_SIZE_MAXIMUM);
251 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Height"));
252 DoValidatedLabel(PosH, aBuf, FONTSIZE, CTouchControls::BUTTON_SIZE_MINIMUM <= H && H <= CTouchControls::BUTTON_SIZE_MAXIMUM);
253
254 // Drop down menu for shapes
255 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
256 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
257 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &RightButton);
258 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Shape"));
259 const char *apShapes[] = {Localize(pStr: "Rectangle", pContext: "Touch button shape"), Localize(pStr: "Circle", pContext: "Touch button shape")};
260 static_assert(std::size(apShapes) == (size_t)CTouchControls::EButtonShape::NUM_SHAPES, "Insufficient shape names");
261 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
262 static CUi::SDropDownState s_ButtonShapeDropDownState;
263 static CScrollRegion s_ButtonShapeDropDownScrollRegion;
264 s_ButtonShapeDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ButtonShapeDropDownScrollRegion;
265 const CTouchControls::EButtonShape NewButtonShape = (CTouchControls::EButtonShape)Ui()->DoDropDown(pRect: &RightButton, CurSelection: (int)m_CachedShape, pStrs: apShapes, Num: (int)CTouchControls::EButtonShape::NUM_SHAPES, State&: s_ButtonShapeDropDownState);
266 if(NewButtonShape != m_CachedShape)
267 {
268 m_CachedShape = NewButtonShape;
269 SetUnsavedChanges(true);
270 Changed = true;
271 }
272 return Changed;
273}
274
275bool CMenusIngameTouchControls::RenderBehaviorSettingBlock(CUIRect Block)
276{
277 const char *apLabelTypes[] = {Localize(pStr: "Plain", pContext: "Touch button label type"), Localize(pStr: "Localized", pContext: "Touch button label type"), Localize(pStr: "Icon", pContext: "Touch button label type")};
278 static_assert(std::size(apLabelTypes) == (size_t)CTouchControls::CButtonLabel::EType::NUM_TYPES, "Insufficient label type names");
279 bool Changed = false;
280 CUIRect EditBox, LeftButton, MiddleButton, RightButton;
281 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
282 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
283 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
284 MiddleButton.VSplitRight(Cut: MiddleButton.h, pLeft: &MiddleButton, pRight: &RightButton);
285 MiddleButton.VSplitRight(Cut: ROWGAP, pLeft: &MiddleButton, pRight: nullptr);
286 char aBuf[128];
287 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Behavior type"));
288 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
289 static CUi::SDropDownState s_ButtonBehaviorDropDownState;
290 static CScrollRegion s_ButtonBehaviorDropDownScrollRegion;
291 s_ButtonBehaviorDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ButtonBehaviorDropDownScrollRegion;
292 const char *apBehaviors[] = {Localize(pStr: "Bind", pContext: "Touch button behavior"), Localize(pStr: "Bind Toggle", pContext: "Touch button behavior"), Localize(pStr: "Predefined", pContext: "Touch button behavior")};
293 const EBehaviorType NewButtonBehavior = (EBehaviorType)Ui()->DoDropDown(pRect: &MiddleButton, CurSelection: (int)m_EditBehaviorType, pStrs: apBehaviors, Num: std::size(apBehaviors), State&: s_ButtonBehaviorDropDownState);
294 if(NewButtonBehavior != m_EditBehaviorType)
295 {
296 m_EditBehaviorType = NewButtonBehavior;
297 if(m_EditBehaviorType == EBehaviorType::BIND)
298 {
299 m_vBehaviorElements[0]->UpdateInputs();
300 }
301 SetUnsavedChanges(true);
302 Changed = true;
303 }
304 static CButtonContainer s_OpenBehaviorHelpButton;
305 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenBehaviorHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &RightButton, Flags: BUTTONFLAG_LEFT))
306 {
307 const char *pHelpMessage = HelpMessageForBehaviorType(Type: m_EditBehaviorType);
308 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"), pMessage: pHelpMessage, pButtonLabel: Localize(pStr: "Ok"));
309 }
310 switch(m_EditBehaviorType)
311 {
312 case EBehaviorType::BIND:
313 {
314 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
315 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
316 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
317 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Command"));
318 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
319 if(Ui()->DoClearableEditBox(pLineInput: &m_vBehaviorElements[0]->m_InputCommand, pRect: &MiddleButton, FontSize: 10.0f))
320 {
321 m_vBehaviorElements[0]->UpdateCommand();
322 SetUnsavedChanges(true);
323 Changed = true;
324 }
325
326 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
327 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
328 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
329 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Label"));
330 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
331 if(Ui()->DoClearableEditBox(pLineInput: &m_vBehaviorElements[0]->m_InputLabel, pRect: &MiddleButton, FontSize: 10.0f))
332 {
333 m_vBehaviorElements[0]->UpdateLabel();
334 SetUnsavedChanges(true);
335 Changed = true;
336 }
337
338 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
339 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
340 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
341 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Label type"));
342 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
343 CTouchControls::CButtonLabel::EType NewButtonLabelType = m_vBehaviorElements[0]->m_CachedCommands.m_LabelType;
344 MiddleButton.VSplitLeft(Cut: MiddleButton.w / 3.0f, pLeft: &LeftButton, pRight: &MiddleButton);
345 MiddleButton.VSplitMid(pLeft: &MiddleButton, pRight: &RightButton);
346 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[0]->m_aLabelTypeRadios[0], pText: apLabelTypes[0], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::PLAIN ? 1 : 0, pRect: &LeftButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_L))
347 NewButtonLabelType = CTouchControls::CButtonLabel::EType::PLAIN;
348 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[0]->m_aLabelTypeRadios[1], pText: apLabelTypes[1], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::LOCALIZED ? 1 : 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_NONE))
349 NewButtonLabelType = CTouchControls::CButtonLabel::EType::LOCALIZED;
350 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[0]->m_aLabelTypeRadios[2], pText: apLabelTypes[2], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::ICON ? 1 : 0, pRect: &RightButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_R))
351 NewButtonLabelType = CTouchControls::CButtonLabel::EType::ICON;
352 if(NewButtonLabelType != m_vBehaviorElements[0]->m_CachedCommands.m_LabelType)
353 {
354 Changed = true;
355 SetUnsavedChanges(true);
356 m_vBehaviorElements[0]->m_CachedCommands.m_LabelType = NewButtonLabelType;
357 }
358 break;
359 }
360 case EBehaviorType::PREDEFINED:
361 {
362 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
363 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
364 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
365 MiddleButton.VSplitRight(Cut: MiddleButton.h, pLeft: &MiddleButton, pRight: &RightButton);
366 MiddleButton.VSplitRight(Cut: ROWGAP, pLeft: &MiddleButton, pRight: nullptr);
367 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Type"));
368 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
369 static CUi::SDropDownState s_ButtonPredefinedDropDownState;
370 static CScrollRegion s_ButtonPredefinedDropDownScrollRegion;
371 const char **apPredefinedNames = PredefinedNames();
372 s_ButtonPredefinedDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ButtonPredefinedDropDownScrollRegion;
373 const EPredefinedType NewPredefined = (EPredefinedType)Ui()->DoDropDown(pRect: &MiddleButton, CurSelection: (int)m_PredefinedBehaviorType, pStrs: apPredefinedNames, Num: std::size(BEHAVIOR_FACTORIES_EDITOR), State&: s_ButtonPredefinedDropDownState);
374 if(NewPredefined != m_PredefinedBehaviorType)
375 {
376 m_PredefinedBehaviorType = NewPredefined;
377 SetUnsavedChanges(true);
378 Changed = true;
379 }
380 static CButtonContainer s_OpenPredefinedBehaviorHelpButton;
381 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenPredefinedBehaviorHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &RightButton, Flags: BUTTONFLAG_LEFT))
382 {
383 const char *pHelpMessage = HelpMessageForPredefinedType(Type: m_PredefinedBehaviorType);
384 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"), pMessage: pHelpMessage, pButtonLabel: Localize(pStr: "Ok"));
385 }
386
387 if(m_PredefinedBehaviorType != EPredefinedType::EXTRA_MENU)
388 break;
389
390 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
391 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
392 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
393 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: &LeftButton, pRight: &MiddleButton);
394 static CButtonContainer s_ExtraMenuDecreaseButton;
395 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ExtraMenuDecreaseButton, pText: FontIcon::MINUS, Checked: 0, pRect: &LeftButton, Flags: BUTTONFLAG_LEFT))
396 {
397 if(m_CachedExtraMenuNumber > 0)
398 {
399 // Menu Number also counts from 1, but written as 0.
400 m_CachedExtraMenuNumber--;
401 SetUnsavedChanges(true);
402 Changed = true;
403 }
404 }
405
406 MiddleButton.VSplitRight(Cut: ROWSIZE, pLeft: &LeftButton, pRight: &MiddleButton);
407 Ui()->DoLabel(pRect: &LeftButton, pText: std::to_string(val: m_CachedExtraMenuNumber + 1).c_str(), Size: FONTSIZE, Align: TEXTALIGN_MC);
408 static CButtonContainer s_ExtraMenuIncreaseButton;
409 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ExtraMenuIncreaseButton, pText: FontIcon::PLUS, Checked: 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT))
410 {
411 if(m_CachedExtraMenuNumber < CTouchControls::MAX_EXTRA_MENU_NUMBER - 1)
412 {
413 m_CachedExtraMenuNumber++;
414 SetUnsavedChanges(true);
415 Changed = true;
416 }
417 }
418 break;
419 }
420 case EBehaviorType::BIND_TOGGLE:
421 {
422 static CScrollRegion s_BindToggleScrollRegion;
423 CScrollRegionParams ScrollParam;
424 ScrollParam.m_ScrollUnit = 90.0f;
425 s_BindToggleScrollRegion.Begin(pClipRect: &Block, pParams: &ScrollParam);
426 for(unsigned CommandIndex = 0; CommandIndex < m_vBehaviorElements.size(); CommandIndex++)
427 {
428 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
429 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
430 {
431 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
432 EditBox.VSplitMid(pLeft: &EditBox, pRight: &RightButton);
433 RightButton.VSplitLeft(Cut: ScrollParam.m_ScrollbarWidth / 2.0f, pLeft: nullptr, pRight: &RightButton);
434 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: &MiddleButton, pRight: &EditBox);
435 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &LeftButton);
436 Ui()->DoLabel(pRect: &LeftButton, pText: Localize(pStr: "Add command"), Size: FONTSIZE, Align: TEXTALIGN_ML);
437 if(Ui()->DoButton_FontIcon(pButtonContainer: &m_vBehaviorElements[CommandIndex]->m_BindToggleAddButtons, pText: FontIcon::PLUS, Checked: 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT))
438 {
439 m_vBehaviorElements.emplace(position: m_vBehaviorElements.begin() + CommandIndex, args: std::make_unique<CBehaviorElements>());
440 m_vBehaviorElements[CommandIndex]->UpdateInputs();
441 Changed = true;
442 SetUnsavedChanges(true);
443 }
444 RightButton.VSplitLeft(Cut: ROWSIZE, pLeft: &MiddleButton, pRight: &RightButton);
445 RightButton.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &LeftButton);
446 Ui()->DoLabel(pRect: &LeftButton, pText: Localize(pStr: "Delete command"), Size: FONTSIZE, Align: TEXTALIGN_ML);
447 if(Ui()->DoButton_FontIcon(pButtonContainer: &m_vBehaviorElements[CommandIndex]->m_BindToggleDeleteButtons, pText: FontIcon::TRASH, Checked: 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT))
448 {
449 if(m_vBehaviorElements.size() > 2)
450 {
451 if(Ui()->CheckActiveItem(pId: &m_vBehaviorElements[CommandIndex]->m_InputCommand) || Ui()->CheckActiveItem(pId: &m_vBehaviorElements[CommandIndex]->m_InputLabel))
452 Ui()->SetActiveItem(nullptr);
453 m_vBehaviorElements.erase(position: m_vBehaviorElements.begin() + CommandIndex);
454 continue;
455 }
456 else
457 {
458 m_vBehaviorElements[CommandIndex]->Reset();
459 }
460 SetUnsavedChanges(true);
461 Changed = true;
462 }
463 }
464 Block.HSplitTop(Cut: ROWGAP, pTop: &EditBox, pBottom: &Block);
465 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
466 {
467 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
468 }
469 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
470 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
471 {
472 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
473 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
474 MiddleButton.VSplitLeft(Cut: ScrollParam.m_ScrollbarWidth / 2.0f, pLeft: nullptr, pRight: &MiddleButton);
475 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Command"));
476 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
477 if(Ui()->DoClearableEditBox(pLineInput: &m_vBehaviorElements[CommandIndex]->m_InputCommand, pRect: &MiddleButton, FontSize: 10.0f))
478 {
479 m_vBehaviorElements[CommandIndex]->UpdateCommand();
480 SetUnsavedChanges(true);
481 Changed = true;
482 }
483 }
484 Block.HSplitTop(Cut: ROWGAP, pTop: &EditBox, pBottom: &Block);
485 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
486 {
487 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
488 }
489 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
490 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
491 {
492 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
493 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
494 MiddleButton.VSplitLeft(Cut: ScrollParam.m_ScrollbarWidth / 2.0f, pLeft: nullptr, pRight: &MiddleButton);
495 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Label"));
496 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
497 if(Ui()->DoClearableEditBox(pLineInput: &m_vBehaviorElements[CommandIndex]->m_InputLabel, pRect: &MiddleButton, FontSize: 10.0f))
498 {
499 m_vBehaviorElements[CommandIndex]->UpdateLabel();
500 SetUnsavedChanges(true);
501 Changed = true;
502 }
503 }
504 Block.HSplitTop(Cut: ROWGAP, pTop: &EditBox, pBottom: &Block);
505 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
506 {
507 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
508 }
509 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
510 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
511 {
512 EditBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
513 EditBox.VSplitMid(pLeft: &LeftButton, pRight: &MiddleButton);
514 MiddleButton.VSplitLeft(Cut: ScrollParam.m_ScrollbarWidth / 2.0f, pLeft: nullptr, pRight: &MiddleButton);
515 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Label type"));
516 Ui()->DoLabel(pRect: &LeftButton, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
517 CTouchControls::CButtonLabel::EType NewButtonLabelType = m_vBehaviorElements[CommandIndex]->m_CachedCommands.m_LabelType;
518 MiddleButton.VSplitLeft(Cut: MiddleButton.w / 3.0f, pLeft: &LeftButton, pRight: &MiddleButton);
519 MiddleButton.VSplitMid(pLeft: &MiddleButton, pRight: &RightButton);
520 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[CommandIndex]->m_aLabelTypeRadios[0], pText: apLabelTypes[0], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::PLAIN ? 1 : 0, pRect: &LeftButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_L))
521 NewButtonLabelType = CTouchControls::CButtonLabel::EType::PLAIN;
522 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[CommandIndex]->m_aLabelTypeRadios[1], pText: apLabelTypes[1], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::LOCALIZED ? 1 : 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_NONE))
523 NewButtonLabelType = CTouchControls::CButtonLabel::EType::LOCALIZED;
524 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &m_vBehaviorElements[CommandIndex]->m_aLabelTypeRadios[2], pText: apLabelTypes[2], Checked: NewButtonLabelType == CTouchControls::CButtonLabel::EType::ICON ? 1 : 0, pRect: &RightButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_R))
525 NewButtonLabelType = CTouchControls::CButtonLabel::EType::ICON;
526 if(NewButtonLabelType != m_vBehaviorElements[CommandIndex]->m_CachedCommands.m_LabelType)
527 {
528 Changed = true;
529 SetUnsavedChanges(true);
530 m_vBehaviorElements[CommandIndex]->m_CachedCommands.m_LabelType = NewButtonLabelType;
531 }
532 }
533 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
534 }
535 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
536 if(s_BindToggleScrollRegion.AddRect(Rect: EditBox))
537 {
538 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: &MiddleButton, pRight: &EditBox);
539 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &LeftButton);
540 Ui()->DoLabel(pRect: &LeftButton, pText: Localize(pStr: "Add command"), Size: FONTSIZE, Align: TEXTALIGN_ML);
541 static CButtonContainer s_FinalAddButton;
542 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_FinalAddButton, pText: FontIcon::PLUS, Checked: 0, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT))
543 {
544 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
545 Changed = true;
546 SetUnsavedChanges(true);
547 }
548 }
549 s_BindToggleScrollRegion.End();
550 break;
551 }
552 default: dbg_assert_failed("Unknown behavior type %d to render.", (int)m_EditBehaviorType);
553 }
554
555 return Changed;
556}
557
558bool CMenusIngameTouchControls::RenderVisibilitySettingBlock(CUIRect Block)
559{
560 // Visibilities time. This is button's visibility, not virtual.
561 bool Changed = false;
562 CUIRect EditBox, LeftButton, MiddleButton, RightButton, Label, HelpButton;
563
564 // Block.HSplitTop(ROWGAP, nullptr, &Block);
565 static CScrollRegion s_VisibilityScrollRegion;
566 CScrollRegionParams ScrollParam;
567 ScrollParam.m_ScrollUnit = 90.0f;
568 s_VisibilityScrollRegion.Begin(pClipRect: &Block, pParams: &ScrollParam);
569
570 static CButtonContainer s_aHelpButtons[(int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES];
571 static std::vector<CButtonContainer> s_avVisibilitySelector[(int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES];
572 if(s_avVisibilitySelector[0].empty())
573 std::for_each_n(first: s_avVisibilitySelector, n: (int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES, f: [](auto &Element) {
574 Element.resize(3);
575 });
576 const char **ppVisibilities = VisibilityNames();
577 for(unsigned Current = 0; Current < (unsigned)CTouchControls::EButtonVisibility::NUM_VISIBILITIES; ++Current)
578 {
579 Block.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &Block);
580 Block.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &Block);
581 if(s_VisibilityScrollRegion.AddRect(Rect: EditBox))
582 {
583 EditBox.VSplitRight(Cut: EditBox.w / 2.0f + ROWGAP + ROWSIZE, pLeft: &Label, pRight: &MiddleButton);
584 MiddleButton.VSplitLeft(Cut: ScrollParam.m_ScrollbarWidth / 2.0f, pLeft: nullptr, pRight: &MiddleButton);
585 MiddleButton.VSplitLeft(Cut: ROWSIZE, pLeft: &HelpButton, pRight: &MiddleButton);
586 MiddleButton.VSplitLeft(Cut: ROWGAP, pLeft: nullptr, pRight: &MiddleButton);
587 // We'll only do help button for the first extra menu visibility.
588 if(Current <= (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1)
589 {
590 const char *pHelpMessage = HelpMessageForVisibilityType(Type: (CTouchControls::EButtonVisibility)Current);
591 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_aHelpButtons[Current], pText: FontIcon::QUESTION, Checked: 0, pRect: &HelpButton, Flags: BUTTONFLAG_LEFT))
592 {
593 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"), pMessage: pHelpMessage, pButtonLabel: Localize(pStr: "Ok"));
594 }
595 }
596 if(Current < (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1)
597 {
598 Ui()->DoLabel(pRect: &Label, pText: ppVisibilities[Current], Size: FONTSIZE, Align: TEXTALIGN_ML);
599 }
600 else
601 {
602 unsigned ExtraMenuNumber = Current - (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1 + 1;
603 char aBuf[64];
604 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d", ppVisibilities[(int)CTouchControls::EButtonVisibility::EXTRA_MENU_1], ExtraMenuNumber);
605 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: FONTSIZE, Align: TEXTALIGN_ML);
606 }
607 MiddleButton.VSplitLeft(Cut: MiddleButton.w / 3.0f, pLeft: &LeftButton, pRight: &MiddleButton);
608 MiddleButton.VSplitMid(pLeft: &MiddleButton, pRight: &RightButton);
609 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_avVisibilitySelector[Current][(int)EVisibilityType::INCLUDE], pText: Localize(pStr: "Included", pContext: "Touch button visibility preview"), Checked: m_aCachedVisibilities[Current] == EVisibilityType::INCLUDE, pRect: &LeftButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_L))
610 {
611 m_aCachedVisibilities[Current] = EVisibilityType::INCLUDE;
612 SetUnsavedChanges(true);
613 Changed = true;
614 }
615 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_avVisibilitySelector[Current][(int)EVisibilityType::EXCLUDE], pText: Localize(pStr: "Excluded", pContext: "Touch button visibility preview"), Checked: m_aCachedVisibilities[Current] == EVisibilityType::EXCLUDE, pRect: &MiddleButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_NONE))
616 {
617 m_aCachedVisibilities[Current] = EVisibilityType::EXCLUDE;
618 SetUnsavedChanges(true);
619 Changed = true;
620 }
621 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_avVisibilitySelector[Current][(int)EVisibilityType::IGNORE], pText: Localize(pStr: "Ignore", pContext: "Touch button visibility preview"), Checked: m_aCachedVisibilities[Current] == EVisibilityType::IGNORE, pRect: &RightButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_R))
622 {
623 m_aCachedVisibilities[Current] = EVisibilityType::IGNORE;
624 SetUnsavedChanges(true);
625 Changed = true;
626 }
627 }
628 }
629 s_VisibilityScrollRegion.End();
630 return Changed;
631}
632
633void CMenusIngameTouchControls::RenderTouchButtonBrowser(CUIRect MainView)
634{
635 CUIRect LeftButton, MiddleButton, RightButton, EditBox, LabelRect, CommandRect, X, Y, W, H, Row;
636 MainView.h = 600.0f - 40.0f - MainView.y;
637 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
638 MainView.Margin(Cut: MAINMARGIN, pOtherRect: &MainView);
639
640 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
641 Row.VSplitLeft(Cut: Row.h, pLeft: nullptr, pRight: &Row);
642 Row.VSplitRight(Cut: Row.h, pLeft: &Row, pRight: &RightButton);
643 Row.VMargin(Cut: 5.0f, pOtherRect: &Row);
644 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Button browser"), Size: TITLESIZE, Align: TEXTALIGN_MC);
645 static CButtonContainer s_OpenHelpButton;
646 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &RightButton, Flags: BUTTONFLAG_LEFT))
647 {
648 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"),
649 pMessage: Localize(pStr: "You can select buttons directly in the list below, or long press on a touch button on the screen to select it. You can also move and resize buttons by touch."),
650 pButtonLabel: Localize(pStr: "Ok"));
651 }
652
653 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
654 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
655 EditBox.VSplitLeft(Cut: (EditBox.w - SUBMARGIN) / 2.0f, pLeft: &LeftButton, pRight: &EditBox);
656 static CButtonContainer s_NewButton;
657 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_NewButton, pText: Localize(pStr: "New button"), Checked: 0, pRect: &LeftButton))
658 {
659 if(NewButton(Rect: {.m_X: 0, .m_Y: 0, .m_W: CTouchControls::BUTTON_SIZE_MINIMUM, .m_H: CTouchControls::BUTTON_SIZE_MINIMUM}, Shape: CTouchControls::EButtonShape::RECT))
660 {
661 const auto Rect = GetPosInputs();
662 ResetCachedSettings();
663 SetPosInputs(Rect);
664 SaveCachedSettingsToTarget(pTargetButton: GameClient()->m_TouchControls.SelectedButton());
665 UpdateSampleButton();
666 m_NeedUpdatePreview = true;
667 GameClient()->m_TouchControls.SetEditingChanges(true);
668 }
669 }
670 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &MiddleButton);
671 static CButtonContainer s_SelectedButton;
672 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_SelectedButton, pText: Localize(pStr: "Select button by touch"), Checked: 0, pRect: &MiddleButton))
673 GameClient()->m_Menus.SetActive(false);
674
675 MainView.HSplitBottom(Cut: ROWSIZE, pTop: &MainView, pBottom: &EditBox);
676 MainView.HSplitBottom(Cut: ROWGAP, pTop: &MainView, pBottom: nullptr);
677 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: &LeftButton, pRight: &EditBox);
678 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
679 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
680 Ui()->DoLabel(pRect: &LeftButton, pText: FontIcon::MAGNIFYING_GLASS, Size: FONTSIZE, Align: TEXTALIGN_ML);
681 TextRender()->SetRenderFlags(0);
682 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
683 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
684 EditBox.VSplitLeft(Cut: EditBox.w / 3.0f, pLeft: &LeftButton, pRight: &EditBox);
685 char aBufSearch[64];
686 str_format(buffer: aBufSearch, buffer_size: sizeof(aBufSearch), format: "%s:", Localize(pStr: "Search"));
687 Ui()->DoLabel(pRect: &LeftButton, pText: aBufSearch, Size: FONTSIZE, Align: TEXTALIGN_ML);
688
689 if(Ui()->DoClearableEditBox(pLineInput: &m_FilterInput, pRect: &EditBox, FontSize: FONTSIZE))
690 m_NeedFilter = true;
691
692 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
693 // Makes the header labels looks better.
694 MainView.HSplitTop(Cut: FONTSIZE / CUi::ms_FontmodHeight, pTop: &EditBox, pBottom: &MainView);
695 static CListBox s_PreviewListBox;
696 EditBox.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
697 EditBox.VSplitRight(Cut: s_PreviewListBox.ScrollbarWidthMax(), pLeft: &EditBox, pRight: nullptr);
698 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: nullptr, pRight: &EditBox);
699
700 float PosWidth = EditBox.w * 4.0f / 9.0f; // Total pos width.
701 const float ButtonWidth = (EditBox.w - PosWidth - SUBMARGIN) / 2.0f;
702 PosWidth = (PosWidth - SUBMARGIN * 4.0f) / 4.0f; // Divided pos width.
703 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &LabelRect, pRight: &EditBox);
704 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
705 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &CommandRect, pRight: &EditBox);
706 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
707 EditBox.VSplitLeft(Cut: ButtonWidth, pLeft: &RightButton, pRight: &EditBox);
708 RightButton.VSplitLeft(Cut: PosWidth, pLeft: &X, pRight: &RightButton);
709 RightButton.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &RightButton);
710 RightButton.VSplitLeft(Cut: PosWidth, pLeft: &Y, pRight: &RightButton);
711 RightButton.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &RightButton);
712 RightButton.VSplitLeft(Cut: PosWidth, pLeft: &W, pRight: &RightButton);
713 RightButton.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &RightButton);
714 RightButton.VSplitLeft(Cut: PosWidth, pLeft: &H, pRight: &RightButton);
715 RightButton.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &RightButton);
716 const std::array<std::pair<CUIRect *, const char *>, (unsigned)ESortType::NUM_SORTS> aHeaderDatas = {
717 ._M_elems: {{&LabelRect, Localize(pStr: "Label")},
718 {&X, "X"},
719 {&Y, "Y"},
720 {&W, Localize(pStr: "Width")},
721 {&H, Localize(pStr: "Height")}}};
722 std::array<std::function<bool(CTouchControls::CTouchButton *, CTouchControls::CTouchButton *)>, (unsigned)ESortType::NUM_SORTS> aSortFunctions = {
723 [](CTouchControls::CTouchButton *pLhs, CTouchControls::CTouchButton *pRhs) {
724 if(pLhs->m_VisibilityCached == pRhs->m_VisibilityCached)
725 return str_comp(a: pLhs->m_pBehavior->GetLabel().m_pLabel, b: pRhs->m_pBehavior->GetLabel().m_pLabel) < 0;
726 return pLhs->m_VisibilityCached;
727 },
728 [](CTouchControls::CTouchButton *pLhs, CTouchControls::CTouchButton *pRhs) {
729 if(pLhs->m_VisibilityCached == pRhs->m_VisibilityCached)
730 return pLhs->m_UnitRect.m_X < pRhs->m_UnitRect.m_X;
731 return pLhs->m_VisibilityCached;
732 },
733 [](CTouchControls::CTouchButton *pLhs, CTouchControls::CTouchButton *pRhs) {
734 if(pLhs->m_VisibilityCached == pRhs->m_VisibilityCached)
735 return pLhs->m_UnitRect.m_Y < pRhs->m_UnitRect.m_Y;
736 return pLhs->m_VisibilityCached;
737 },
738 [](CTouchControls::CTouchButton *pLhs, CTouchControls::CTouchButton *pRhs) {
739 if(pLhs->m_VisibilityCached == pRhs->m_VisibilityCached)
740 return pLhs->m_UnitRect.m_W < pRhs->m_UnitRect.m_W;
741 return pLhs->m_VisibilityCached;
742 },
743 [](CTouchControls::CTouchButton *pLhs, CTouchControls::CTouchButton *pRhs) {
744 if(pLhs->m_VisibilityCached == pRhs->m_VisibilityCached)
745 return pLhs->m_UnitRect.m_H < pRhs->m_UnitRect.m_H;
746 return pLhs->m_VisibilityCached;
747 }};
748 for(unsigned HeaderIndex = (unsigned)ESortType::LABEL; HeaderIndex < (unsigned)ESortType::NUM_SORTS; HeaderIndex++)
749 {
750 if(GameClient()->m_Menus.DoButton_GridHeader(pId: &m_aSortHeaderIds[HeaderIndex], pText: "",
751 Checked: (unsigned)m_SortType == HeaderIndex, pRect: aHeaderDatas[HeaderIndex].first))
752 {
753 if(m_SortType != (ESortType)HeaderIndex)
754 {
755 m_NeedSort = true;
756 m_SortType = (ESortType)HeaderIndex;
757 }
758 }
759 Ui()->DoLabel(pRect: aHeaderDatas[HeaderIndex].first, pText: aHeaderDatas[HeaderIndex].second, Size: FONTSIZE, Align: TEXTALIGN_ML);
760 }
761 // Can't sort buttons basing on command, that's meaningless and too slow.
762 Ui()->DoLabel(pRect: &CommandRect, pText: Localize(pStr: "Command"), Size: FONTSIZE, Align: TEXTALIGN_ML);
763
764 if(m_NeedUpdatePreview)
765 {
766 m_NeedUpdatePreview = false;
767 m_vpButtons = GameClient()->m_TouchControls.GetButtonsEditor();
768 m_NeedSort = true;
769 m_NeedFilter = true;
770 }
771
772 if(m_NeedFilter || m_NeedSort)
773 {
774 // Sorting can be done directly.
775 if(m_NeedSort)
776 {
777 m_NeedSort = false;
778 m_NeedFilter = true;
779 std::sort(first: m_vpButtons.begin(), last: m_vpButtons.end(), comp: aSortFunctions[(unsigned)m_SortType]);
780 }
781
782 m_vpMutableButtons = m_vpButtons;
783
784 // Filtering should be done separately.
785 if(m_NeedFilter && !m_FilterInput.IsEmpty())
786 {
787 // Both label and command will be considered.
788 const auto DeleteIt = std::remove_if(first: m_vpMutableButtons.begin(), last: m_vpMutableButtons.end(), pred: [&](CTouchControls::CTouchButton *pButton) {
789 if(str_utf8_find_nocase(haystack: pButton->m_pBehavior->GetLabel().m_pLabel, needle: m_FilterInput.GetString()) != nullptr)
790 return false;
791 std::string Command = DetermineTouchButtonCommandLabel(pButton);
792 if(str_utf8_find_nocase(haystack: Command.c_str(), needle: m_FilterInput.GetString()) != nullptr)
793 return false;
794 return true;
795 });
796 m_vpMutableButtons.erase(first: DeleteIt, last: m_vpMutableButtons.end());
797 }
798 m_NeedFilter = false;
799 }
800
801 if(!m_vpMutableButtons.empty())
802 {
803 s_PreviewListBox.SetActive(true);
804 s_PreviewListBox.DoStart(RowHeight: ROWSIZE, NumItems: m_vpMutableButtons.size(), ItemsPerRow: 1, RowsPerScroll: 4, SelectedIndex: m_SelectedPreviewButtonIndex, pRect: &MainView, Background: true, BackgroundCorners: IGraphics::CORNER_B);
805 for(unsigned ButtonIndex = 0; ButtonIndex < m_vpMutableButtons.size(); ButtonIndex++)
806 {
807 CTouchControls::CTouchButton *pButton = m_vpMutableButtons[ButtonIndex];
808 const CListboxItem ListItem = s_PreviewListBox.DoNextItem(pId: &m_vpMutableButtons[ButtonIndex], Selected: m_SelectedPreviewButtonIndex == (int)ButtonIndex);
809 if(ListItem.m_Visible)
810 {
811 EditBox = ListItem.m_Rect;
812 EditBox.VSplitLeft(Cut: ROWSIZE, pLeft: &LeftButton, pRight: &EditBox);
813 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
814 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
815 Ui()->DoLabel(pRect: &LeftButton, pText: m_vpMutableButtons[ButtonIndex]->m_VisibilityCached ? FontIcon::EYE : FontIcon::EYE_SLASH, Size: FONTSIZE, Align: TEXTALIGN_ML);
816 TextRender()->SetRenderFlags(0);
817 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
818 EditBox.VSplitLeft(Cut: LabelRect.w, pLeft: &LeftButton, pRight: &EditBox);
819 const char *pLabel = pButton->m_pBehavior->GetLabel().m_pLabel;
820 const auto LabelType = pButton->m_pBehavior->GetLabel().m_Type;
821 if(LabelType == CTouchControls::CButtonLabel::EType::PLAIN)
822 {
823 SLabelProperties Props;
824 Props.m_MaxWidth = LeftButton.w;
825 Props.m_EnableWidthCheck = false;
826 Props.m_EllipsisAtEnd = true;
827 Ui()->DoLabel(pRect: &LeftButton, pText: pLabel, Size: FONTSIZE, Align: TEXTALIGN_ML, LabelProps: Props);
828 }
829 else if(LabelType == CTouchControls::CButtonLabel::EType::LOCALIZED)
830 {
831 pLabel = Localize(pStr: pLabel);
832 SLabelProperties Props;
833 Props.m_MaxWidth = LeftButton.w;
834 Props.m_EnableWidthCheck = false;
835 Props.m_EllipsisAtEnd = true;
836 Ui()->DoLabel(pRect: &LeftButton, pText: pLabel, Size: FONTSIZE, Align: TEXTALIGN_ML, LabelProps: Props);
837 }
838 else if(LabelType == CTouchControls::CButtonLabel::EType::ICON)
839 {
840 SLabelProperties Props;
841 Props.m_MaxWidth = LeftButton.w;
842 Props.m_EnableWidthCheck = false;
843 Props.m_EllipsisAtEnd = true;
844 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
845 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
846 Ui()->DoLabel(pRect: &LeftButton, pText: pLabel, Size: FONTSIZE, Align: TEXTALIGN_ML, LabelProps: Props);
847 TextRender()->SetRenderFlags(0);
848 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
849 }
850 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
851 EditBox.VSplitLeft(Cut: CommandRect.w, pLeft: &LeftButton, pRight: &EditBox);
852 std::string Command = DetermineTouchButtonCommandLabel(pButton);
853 SLabelProperties Props;
854 Props.m_MaxWidth = LeftButton.w;
855 Props.m_EnableWidthCheck = false;
856 Props.m_EllipsisAtEnd = true;
857 Ui()->DoLabel(pRect: &LeftButton, pText: Command.c_str(), Size: FONTSIZE, Align: TEXTALIGN_ML, LabelProps: Props);
858 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
859 EditBox.VSplitLeft(Cut: X.w, pLeft: &LeftButton, pRight: &EditBox);
860 Ui()->DoLabel(pRect: &LeftButton, pText: std::to_string(val: pButton->m_UnitRect.m_X).c_str(), Size: FONTSIZE, Align: TEXTALIGN_ML);
861 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
862 EditBox.VSplitLeft(Cut: Y.w, pLeft: &LeftButton, pRight: &EditBox);
863 Ui()->DoLabel(pRect: &LeftButton, pText: std::to_string(val: pButton->m_UnitRect.m_Y).c_str(), Size: FONTSIZE, Align: TEXTALIGN_ML);
864 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
865 EditBox.VSplitLeft(Cut: W.w, pLeft: &LeftButton, pRight: &EditBox);
866 Ui()->DoLabel(pRect: &LeftButton, pText: std::to_string(val: pButton->m_UnitRect.m_W).c_str(), Size: FONTSIZE, Align: TEXTALIGN_ML);
867 EditBox.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &EditBox);
868 EditBox.VSplitLeft(Cut: H.w, pLeft: &LeftButton, pRight: &EditBox);
869 Ui()->DoLabel(pRect: &LeftButton, pText: std::to_string(val: pButton->m_UnitRect.m_H).c_str(), Size: FONTSIZE, Align: TEXTALIGN_ML);
870 }
871 }
872
873 m_SelectedPreviewButtonIndex = s_PreviewListBox.DoEnd();
874 if(s_PreviewListBox.WasItemActivated())
875 {
876 GameClient()->m_TouchControls.SetSelectedButton(m_vpMutableButtons[m_SelectedPreviewButtonIndex]);
877 CacheAllSettingsFromTarget(pTargetButton: m_vpMutableButtons[m_SelectedPreviewButtonIndex]);
878 SetUnsavedChanges(false);
879 UpdateSampleButton();
880 }
881 }
882 else
883 {
884 // Copied from menus_browser.cpp
885 MainView.HMargin(Cut: (MainView.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &LeftButton);
886 LeftButton.HSplitTop(Cut: 16.0f, pTop: &LeftButton, pBottom: &MiddleButton);
887 MiddleButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &MiddleButton);
888 MiddleButton.VMargin(Cut: (MiddleButton.w - 200.0f) / 2.0f, pOtherRect: &MiddleButton);
889 Ui()->DoLabel(pRect: &LeftButton, pText: Localize(pStr: "No buttons match your filter criteria"), Size: 16.0f, Align: TEXTALIGN_MC);
890 static CButtonContainer s_ResetButton;
891 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &MiddleButton))
892 {
893 m_FilterInput.Clear();
894 m_NeedUpdatePreview = true;
895 }
896 }
897}
898
899void CMenusIngameTouchControls::RenderSelectingTab(CUIRect SelectingTab)
900{
901 CUIRect LeftButton;
902 SelectingTab.VSplitLeft(Cut: SelectingTab.w / 4.0f, pLeft: &LeftButton, pRight: &SelectingTab);
903 static CButtonContainer s_FileTab;
904 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &s_FileTab, pText: Localize(pStr: "File"), Checked: m_CurrentMenu == EMenuType::MENU_FILE, pRect: &LeftButton, Corners: IGraphics::CORNER_TL))
905 m_CurrentMenu = EMenuType::MENU_FILE;
906 SelectingTab.VSplitLeft(Cut: SelectingTab.w / 3.0f, pLeft: &LeftButton, pRight: &SelectingTab);
907 static CButtonContainer s_ButtonTab;
908 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &s_ButtonTab, pText: Localize(pStr: "Buttons"), Checked: m_CurrentMenu == EMenuType::MENU_BUTTONS, pRect: &LeftButton, Corners: IGraphics::CORNER_NONE))
909 m_CurrentMenu = EMenuType::MENU_BUTTONS;
910 SelectingTab.VSplitLeft(Cut: SelectingTab.w / 2.0f, pLeft: &LeftButton, pRight: &SelectingTab);
911 static CButtonContainer s_SettingsMenuTab;
912 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &s_SettingsMenuTab, pText: Localize(pStr: "Settings"), Checked: m_CurrentMenu == EMenuType::MENU_SETTINGS, pRect: &LeftButton, Corners: IGraphics::CORNER_NONE))
913 m_CurrentMenu = EMenuType::MENU_SETTINGS;
914 SelectingTab.VSplitLeft(Cut: SelectingTab.w / 1.0f, pLeft: &LeftButton, pRight: &SelectingTab);
915 static CButtonContainer s_PreviewTab;
916 if(GameClient()->m_Menus.DoButton_MenuTab(pButtonContainer: &s_PreviewTab, pText: Localize(pStr: "Preview"), Checked: m_CurrentMenu == EMenuType::MENU_PREVIEW, pRect: &LeftButton, Corners: IGraphics::CORNER_TR))
917 m_CurrentMenu = EMenuType::MENU_PREVIEW;
918}
919
920void CMenusIngameTouchControls::RenderConfigSettings(CUIRect MainView)
921{
922 CUIRect EditBox, Row, Label, Button;
923 MainView.h = 2 * MAINMARGIN + 4 * ROWSIZE + 3 * ROWGAP;
924 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
925 MainView.VMargin(Cut: MAINMARGIN, pOtherRect: &MainView);
926 MainView.HSplitTop(Cut: MAINMARGIN, pTop: nullptr, pBottom: &MainView);
927 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
928 static CButtonContainer s_ActiveColorPicker;
929 ColorHSLA ColorTest = GameClient()->m_Menus.DoLine_ColorPicker(pResetId: &s_ActiveColorPicker, LineSize: ROWSIZE, LabelSize: FONTSIZE, BottomMargin: 5.0f, pMainRect: &EditBox, pText: Localize(pStr: "Active color"), pColorValue: &m_ColorActive, DefaultColor: GameClient()->m_TouchControls.DefaultBackgroundColorActive(), CheckBoxSpacing: false, pCheckBoxValue: nullptr, Alpha: true);
930 GameClient()->m_TouchControls.SetBackgroundColorActive(color_cast<ColorRGBA>(hsl: ColorHSLA(m_ColorActive, true)));
931 if(color_cast<ColorRGBA>(hsl: ColorTest) != GameClient()->m_TouchControls.BackgroundColorActive())
932 GameClient()->m_TouchControls.SetEditingChanges(true);
933
934 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
935 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
936 static CButtonContainer s_InactiveColorPicker;
937 ColorTest = GameClient()->m_Menus.DoLine_ColorPicker(pResetId: &s_InactiveColorPicker, LineSize: ROWSIZE, LabelSize: FONTSIZE, BottomMargin: 5.0f, pMainRect: &EditBox, pText: Localize(pStr: "Inactive color"), pColorValue: &m_ColorInactive, DefaultColor: GameClient()->m_TouchControls.DefaultBackgroundColorInactive(), CheckBoxSpacing: false, pCheckBoxValue: nullptr, Alpha: true);
938 GameClient()->m_TouchControls.SetBackgroundColorInactive(color_cast<ColorRGBA>(hsl: ColorHSLA(m_ColorInactive, true)));
939 if(color_cast<ColorRGBA>(hsl: ColorTest) != GameClient()->m_TouchControls.BackgroundColorInactive())
940 GameClient()->m_TouchControls.SetEditingChanges(true);
941
942 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
943 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
944 Row.VSplitMid(pLeft: &Label, pRight: &Button);
945 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Direct touch input while ingame"), Size: FONTSIZE, Align: TEXTALIGN_ML);
946
947 const char *apIngameTouchModes[(int)CTouchControls::EDirectTouchIngameMode::NUM_STATES] = {Localize(pStr: "Disabled", pContext: "Direct touch input"), Localize(pStr: "Active action", pContext: "Direct touch input"), Localize(pStr: "Aim", pContext: "Direct touch input"), Localize(pStr: "Fire", pContext: "Direct touch input"), Localize(pStr: "Hook", pContext: "Direct touch input")};
948 const CTouchControls::EDirectTouchIngameMode OldDirectTouchIngame = GameClient()->m_TouchControls.DirectTouchIngame();
949 static CUi::SDropDownState s_DirectTouchIngameDropDownState;
950 static CScrollRegion s_DirectTouchIngameDropDownScrollRegion;
951 s_DirectTouchIngameDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchIngameDropDownScrollRegion;
952 const CTouchControls::EDirectTouchIngameMode NewDirectTouchIngame = (CTouchControls::EDirectTouchIngameMode)Ui()->DoDropDown(pRect: &Button, CurSelection: (int)OldDirectTouchIngame, pStrs: apIngameTouchModes, Num: std::size(apIngameTouchModes), State&: s_DirectTouchIngameDropDownState);
953 if(OldDirectTouchIngame != NewDirectTouchIngame)
954 {
955 GameClient()->m_TouchControls.SetDirectTouchIngame(NewDirectTouchIngame);
956 }
957
958 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
959 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
960 Row.VSplitMid(pLeft: &Label, pRight: &Button);
961 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Direct touch input while spectating"), Size: FONTSIZE, Align: TEXTALIGN_ML);
962
963 const char *apSpectateTouchModes[(int)CTouchControls::EDirectTouchSpectateMode::NUM_STATES] = {Localize(pStr: "Disabled", pContext: "Direct touch input"), Localize(pStr: "Aim", pContext: "Direct touch input")};
964 const CTouchControls::EDirectTouchSpectateMode OldDirectTouchSpectate = GameClient()->m_TouchControls.DirectTouchSpectate();
965 static CUi::SDropDownState s_DirectTouchSpectateDropDownState;
966 static CScrollRegion s_DirectTouchSpectateDropDownScrollRegion;
967 s_DirectTouchSpectateDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchSpectateDropDownScrollRegion;
968 const CTouchControls::EDirectTouchSpectateMode NewDirectTouchSpectate = (CTouchControls::EDirectTouchSpectateMode)Ui()->DoDropDown(pRect: &Button, CurSelection: (int)OldDirectTouchSpectate, pStrs: apSpectateTouchModes, Num: std::size(apSpectateTouchModes), State&: s_DirectTouchSpectateDropDownState);
969 if(OldDirectTouchSpectate != NewDirectTouchSpectate)
970 {
971 GameClient()->m_TouchControls.SetDirectTouchSpectate(NewDirectTouchSpectate);
972 }
973}
974
975void CMenusIngameTouchControls::RenderPreviewSettings(CUIRect MainView)
976{
977 CUIRect EditBox, Button;
978 MainView.h = 600.0f - 40.0f - MainView.y;
979 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
980 MainView.Margin(Cut: MAINMARGIN, pOtherRect: &MainView);
981 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
982 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
983 EditBox.VSplitLeft(Cut: EditBox.h, pLeft: nullptr, pRight: &EditBox);
984 EditBox.VSplitRight(Cut: EditBox.h, pLeft: &EditBox, pRight: &Button);
985 EditBox.VMargin(Cut: 5.0f, pOtherRect: &EditBox);
986 Ui()->DoLabel(pRect: &EditBox, pText: Localize(pStr: "Preview visibilities"), Size: TITLESIZE, Align: TEXTALIGN_MC);
987 static CButtonContainer s_OpenHelpButton;
988 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
989 {
990 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"), pMessage: Localize(pStr: "Preview button visibility while the editor is active."), pButtonLabel: Localize(pStr: "Ok"));
991 }
992
993 MainView.HSplitBottom(Cut: ROWSIZE, pTop: &MainView, pBottom: &EditBox);
994 MainView.HSplitBottom(Cut: ROWGAP, pTop: &MainView, pBottom: nullptr);
995 EditBox.VSplitLeft(Cut: MAINMARGIN, pLeft: nullptr, pRight: &EditBox);
996 static CButtonContainer s_PreviewAllCheckBox;
997 bool Preview = GameClient()->m_TouchControls.PreviewAllButtons();
998 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &s_PreviewAllCheckBox, pText: Localize(pStr: "Show all buttons"), Checked: Preview ? 1 : 0, pRect: &EditBox))
999 {
1000 GameClient()->m_TouchControls.SetPreviewAllButtons(!Preview);
1001 }
1002 MainView.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1003 MainView.VMargin(Cut: MAINMARGIN, pOtherRect: &MainView);
1004 MainView.HMargin(Cut: ROWGAP, pOtherRect: &MainView);
1005 static CScrollRegion s_VirtualVisibilityScrollRegion;
1006 CScrollRegionParams ScrollParam;
1007 ScrollParam.m_ScrollUnit = 90.0f;
1008 s_VirtualVisibilityScrollRegion.Begin(pClipRect: &MainView, pParams: &ScrollParam);
1009 std::array<bool, (size_t)CTouchControls::EButtonVisibility::NUM_VISIBILITIES> aVirtualVisibilities = GameClient()->m_TouchControls.VirtualVisibilities();
1010 const char **ppVisibilities = VisibilityNames();
1011 for(unsigned Current = 0; Current < (unsigned)CTouchControls::EButtonVisibility::NUM_VISIBILITIES; ++Current)
1012 {
1013 MainView.HSplitTop(Cut: ROWSIZE + ROWGAP, pTop: &EditBox, pBottom: &MainView);
1014 if(s_VirtualVisibilityScrollRegion.AddRect(Rect: EditBox))
1015 {
1016 EditBox.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &EditBox);
1017 if(Current < (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1)
1018 {
1019 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aVisibilityIds[Current], pText: ppVisibilities[Current], Checked: aVirtualVisibilities[Current] == 1, pRect: &EditBox))
1020 GameClient()->m_TouchControls.ReverseVirtualVisibilities(Number: Current);
1021 }
1022 else
1023 {
1024 unsigned ExtraMenuNumber = Current - (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1 + 1;
1025 char aBuf[64];
1026 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d", ppVisibilities[(int)CTouchControls::EButtonVisibility::EXTRA_MENU_1], ExtraMenuNumber);
1027 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aVisibilityIds[Current], pText: aBuf, Checked: aVirtualVisibilities[Current] == 1, pRect: &EditBox))
1028 GameClient()->m_TouchControls.ReverseVirtualVisibilities(Number: Current);
1029 }
1030 }
1031 }
1032 s_VirtualVisibilityScrollRegion.End();
1033}
1034
1035void CMenusIngameTouchControls::RenderTouchControlsEditor(CUIRect MainView)
1036{
1037 CUIRect Label, Button, Row;
1038 MainView.h = 2 * MAINMARGIN + 4 * ROWSIZE + 3 * ROWGAP;
1039 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1040 MainView.Margin(Cut: MAINMARGIN, pOtherRect: &MainView);
1041
1042 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1043 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1044 Row.VSplitLeft(Cut: Row.h, pLeft: nullptr, pRight: &Row);
1045 Row.VSplitRight(Cut: Row.h, pLeft: &Row, pRight: &Button);
1046 Row.VMargin(Cut: 5.0f, pOtherRect: &Label);
1047 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Edit touch controls"), Size: TITLESIZE, Align: TEXTALIGN_MC);
1048
1049 static CButtonContainer s_OpenHelpButton;
1050 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
1051 {
1052 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Info"),
1053 pMessage: Localize(pStr: "You can manage your touch controls settings on this page. Only changes that are saved will be available after restarting the client. You can share your touch controls with others by exporting them to the clipboard.\n\nYou can find more detailed information about the touch controls on the DDNet Wiki."),
1054 pConfirmButtonLabel: Localize(pStr: "Open DDNet Wiki"), pCancelButtonLabel: Localize(pStr: "Ok"), pfnConfirmButtonCallback: &CMenus::PopupConfirmOpenWiki);
1055 }
1056
1057 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1058 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1059
1060 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1061 static CButtonContainer s_SaveConfigurationButton;
1062 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_SaveConfigurationButton, pText: Localize(pStr: "Save changes"), Checked: GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, pRect: &Button))
1063 {
1064 if(GameClient()->m_TouchControls.SaveConfigurationToFile())
1065 {
1066 GameClient()->m_TouchControls.SetEditingChanges(false);
1067 }
1068 else
1069 {
1070 SWarning Warning(Localize(pStr: "Error saving touch controls"), Localize(pStr: "Could not save touch controls to file. See local console for details."));
1071 Warning.m_AutoHide = false;
1072 Client()->AddWarning(Warning);
1073 }
1074 }
1075
1076 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1077 if(GameClient()->m_TouchControls.HasEditingChanges())
1078 {
1079 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
1080 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Unsaved changes"), Size: 14.0f, Align: TEXTALIGN_MC);
1081 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1082 }
1083
1084 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1085 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1086
1087 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1088 static CButtonContainer s_DiscardChangesButton;
1089 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_DiscardChangesButton, pText: Localize(pStr: "Discard changes"), Checked: GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, pRect: &Button))
1090 {
1091 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Discard changes"),
1092 pMessage: Localize(pStr: "Are you sure that you want to discard the current changes to the touch controls?"),
1093 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1094 pfnConfirmButtonCallback: &CMenus::PopupConfirmDiscardTouchControlsChanges);
1095 }
1096
1097 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1098 static CButtonContainer s_ResetButton;
1099 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &Button))
1100 {
1101 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Reset to defaults"),
1102 pMessage: Localize(pStr: "Are you sure that you want to reset the touch controls to default?"),
1103 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1104 pfnConfirmButtonCallback: &CMenus::PopupConfirmResetTouchControls);
1105 }
1106
1107 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1108 MainView.HSplitTop(Cut: MAINMARGIN, pTop: nullptr, pBottom: &MainView);
1109
1110 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1111 static CButtonContainer s_ClipboardImportButton;
1112 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ClipboardImportButton, pText: Localize(pStr: "Import from clipboard"), Checked: 0, pRect: &Button))
1113 {
1114 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Import from clipboard"),
1115 pMessage: Localize(pStr: "Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls."),
1116 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1117 pfnConfirmButtonCallback: &CMenus::PopupConfirmImportTouchControlsClipboard);
1118 }
1119
1120 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1121 static CButtonContainer s_ClipboardExportButton;
1122 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ClipboardExportButton, pText: Localize(pStr: "Export to clipboard"), Checked: 0, pRect: &Button))
1123 {
1124 GameClient()->m_TouchControls.SaveConfigurationToClipboard();
1125 }
1126}
1127
1128// Check if CTouchControls need CMenus to open any popups.
1129void CMenusIngameTouchControls::DoPopupType(CTouchControls::CPopupParam PopupParam)
1130{
1131 m_pOldSelectedButton = PopupParam.m_pOldSelectedButton;
1132 m_pNewSelectedButton = PopupParam.m_pNewSelectedButton;
1133 m_CloseMenu = !PopupParam.m_KeepMenuOpen;
1134 switch(PopupParam.m_PopupType)
1135 {
1136 case CTouchControls::EPopupType::BUTTON_CHANGED: ChangeSelectedButtonWhileHavingUnsavedChanges(); break;
1137 case CTouchControls::EPopupType::NO_SPACE: NoSpaceForOverlappingButton(); break;
1138 case CTouchControls::EPopupType::BUTTON_INVISIBLE: SelectedButtonNotVisible(); break;
1139 // The NUM_POPUPS will not call the function.
1140 default: dbg_assert_failed("Unknown popup type = %d.", (int)PopupParam.m_PopupType);
1141 }
1142}
1143
1144void CMenusIngameTouchControls::ChangeSelectedButtonWhileHavingUnsavedChanges() const
1145{
1146 // Both old and new button pointer can be nullptr.
1147 // Saving settings to the old selected button(nullptr = create), then switch to new selected button(new = haven't created).
1148 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Unsaved changes"), pMessage: Localize(pStr: "Save all changes before switching selected button?"), pConfirmButtonLabel: Localize(pStr: "Save"), pCancelButtonLabel: Localize(pStr: "Discard"), pfnConfirmButtonCallback: &CMenus::PopupConfirmChangeSelectedButton, ConfirmNextPopup: CMenus::POPUP_NONE, pfnCancelButtonCallback: &CMenus::PopupCancelChangeSelectedButton);
1149}
1150
1151void CMenusIngameTouchControls::NoSpaceForOverlappingButton() const
1152{
1153 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "No space for button"), pMessage: Localize(pStr: "There is not enough space available for the button. Check its visibilities and size."), pButtonLabel: Localize(pStr: "Ok"));
1154}
1155
1156void CMenusIngameTouchControls::SelectedButtonNotVisible()
1157{
1158 // Cancel shouldn't do anything but open ingame menu, the menu is already opened now.
1159 m_CloseMenu = false;
1160 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Selected button not visible"), pMessage: Localize(pStr: "The selected button is not visible. Do you want to deselect it or edit its visibility?"), pConfirmButtonLabel: Localize(pStr: "Deselect"), pCancelButtonLabel: Localize(pStr: "Edit"), pfnConfirmButtonCallback: &CMenus::PopupConfirmSelectedNotVisible);
1161}
1162
1163bool CMenusIngameTouchControls::UnsavedChanges() const
1164{
1165 return GameClient()->m_TouchControls.HasUnsavedChanges();
1166}
1167
1168void CMenusIngameTouchControls::SetUnsavedChanges(bool UnsavedChanges)
1169{
1170 GameClient()->m_TouchControls.SetUnsavedChanges(UnsavedChanges);
1171}
1172
1173// Check if cached settings are legal.
1174bool CMenusIngameTouchControls::CheckCachedSettings() const
1175{
1176 std::vector<const char *> vpErrors;
1177 char aBuf[256];
1178 auto [X, Y, W, H] = GetPosInputs();
1179 // Illegal size settings.
1180 if(W < CTouchControls::BUTTON_SIZE_MINIMUM || W > CTouchControls::BUTTON_SIZE_MAXIMUM || H < CTouchControls::BUTTON_SIZE_MINIMUM || H > CTouchControls::BUTTON_SIZE_MAXIMUM)
1181 {
1182 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Width and height are required to be within the range from %d to %d."), CTouchControls::BUTTON_SIZE_MINIMUM, CTouchControls::BUTTON_SIZE_MAXIMUM);
1183 vpErrors.emplace_back(args&: aBuf);
1184 }
1185 if(X < 0 || Y < 0 || X + W > CTouchControls::BUTTON_SIZE_SCALE || Y + H > CTouchControls::BUTTON_SIZE_SCALE)
1186 {
1187 vpErrors.emplace_back(args: Localize(pStr: "Button position is outside of the screen."));
1188 }
1189 if(GameClient()->m_TouchControls.IsRectOverlapping(MyRect: {.m_X: X, .m_Y: Y, .m_W: W, .m_H: H}, Shape: m_CachedShape))
1190 {
1191 vpErrors.emplace_back(args: Localize(pStr: "The selected button is overlapping with other buttons."));
1192 }
1193 if(!vpErrors.empty())
1194 {
1195 char aErrorMessage[1024] = "";
1196 for(const char *pError : vpErrors)
1197 {
1198 if(aErrorMessage[0] != '\0')
1199 str_append(dst&: aErrorMessage, src: "\n");
1200 str_append(dst&: aErrorMessage, src: pError);
1201 }
1202 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Wrong button settings"), pMessage: aErrorMessage, pButtonLabel: Localize(pStr: "Ok"));
1203 return false;
1204 }
1205 else
1206 {
1207 return true;
1208 }
1209}
1210
1211// All default settings are here.
1212void CMenusIngameTouchControls::ResetCachedSettings()
1213{
1214 // Reset all cached values.
1215 m_EditBehaviorType = EBehaviorType::BIND;
1216 m_PredefinedBehaviorType = EPredefinedType::EXTRA_MENU;
1217 m_CachedExtraMenuNumber = 0;
1218 m_vBehaviorElements.clear();
1219 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1220 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1221 m_aCachedVisibilities.fill(u: EVisibilityType::IGNORE); // 2 means don't have the visibility, true:1,false:0
1222 m_aCachedVisibilities[(int)CTouchControls::EButtonVisibility::DEMO_PLAYER] = EVisibilityType::EXCLUDE;
1223 // These things can't be empty. std::stoi can't cast empty string.
1224 SetPosInputs({.m_X: 0, .m_Y: 0, .m_W: CTouchControls::BUTTON_SIZE_MINIMUM, .m_H: CTouchControls::BUTTON_SIZE_MINIMUM});
1225 m_CachedShape = CTouchControls::EButtonShape::RECT;
1226}
1227
1228// This is called when the Touch button editor is rendered as well when selectedbutton changes. Used for updating all cached settings.
1229void CMenusIngameTouchControls::CacheAllSettingsFromTarget(CTouchControls::CTouchButton *pTargetButton)
1230{
1231 ResetCachedSettings();
1232 if(pTargetButton == nullptr)
1233 {
1234 return; // Nothing to cache.
1235 }
1236 // These values can't be null. The constructor has been updated. Default:{0,0,CTouchControls::BUTTON_SIZE_MINIMUM,CTouchControls::BUTTON_SIZE_MINIMUM}, shape = rect.
1237 SetPosInputs(pTargetButton->m_UnitRect);
1238 m_CachedShape = pTargetButton->m_Shape;
1239 for(const auto &Visibility : pTargetButton->m_vVisibilities)
1240 {
1241 dbg_assert((int)Visibility.m_Type < (int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES, "Target button has out of bound visibility type: %d", (int)Visibility.m_Type);
1242 m_aCachedVisibilities[(int)Visibility.m_Type] = Visibility.m_Parity ? EVisibilityType::INCLUDE : EVisibilityType::EXCLUDE;
1243 }
1244
1245 // These are behavior values.
1246 if(pTargetButton->m_pBehavior != nullptr)
1247 {
1248 const char *pBehaviorType = pTargetButton->m_pBehavior->GetBehaviorType();
1249 if(str_comp(a: pBehaviorType, b: CTouchControls::CBindTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1250 {
1251 m_EditBehaviorType = EBehaviorType::BIND;
1252 auto *pTargetBehavior = static_cast<CTouchControls::CBindTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1253 // m_LabelType must not be null. Default value is PLAIN.
1254 m_vBehaviorElements[0]->m_CachedCommands = {pTargetBehavior->GetLabel().m_pLabel, pTargetBehavior->GetLabel().m_Type, pTargetBehavior->GetCommand()};
1255 m_vBehaviorElements[0]->UpdateInputs();
1256 }
1257 else if(str_comp(a: pBehaviorType, b: CTouchControls::CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1258 {
1259 m_EditBehaviorType = EBehaviorType::BIND_TOGGLE;
1260 auto *pTargetBehavior = static_cast<CTouchControls::CBindToggleTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1261 auto TargetCommands = pTargetBehavior->GetCommand();
1262 // Can't use resize here :(
1263 while(m_vBehaviorElements.size() > maximum<size_t>(a: TargetCommands.size(), b: 2))
1264 m_vBehaviorElements.pop_back();
1265 while(m_vBehaviorElements.size() < maximum<size_t>(a: TargetCommands.size(), b: 2))
1266 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1267 for(unsigned CommandIndex = 0; CommandIndex < TargetCommands.size(); CommandIndex++)
1268 {
1269 m_vBehaviorElements[CommandIndex]->m_CachedCommands = TargetCommands[CommandIndex];
1270 m_vBehaviorElements[CommandIndex]->UpdateInputs();
1271 }
1272 }
1273 else if(str_comp(a: pBehaviorType, b: CTouchControls::CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1274 {
1275 m_EditBehaviorType = EBehaviorType::PREDEFINED;
1276 auto *pTargetBehavior = static_cast<CTouchControls::CPredefinedTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1277 const char *pPredefinedType = pTargetBehavior->GetPredefinedType();
1278 if(pPredefinedType == nullptr)
1279 m_PredefinedBehaviorType = EPredefinedType::EXTRA_MENU;
1280 else
1281 m_PredefinedBehaviorType = (EPredefinedType)CalculatePredefinedType(pType: pPredefinedType);
1282 dbg_assert(m_PredefinedBehaviorType != EPredefinedType::NUM_PREDEFINEDTYPES, "Detected out of bound m_PredefinedBehaviorType. pPredefinedType = %s", pPredefinedType);
1283
1284 if(m_PredefinedBehaviorType == EPredefinedType::EXTRA_MENU)
1285 {
1286 auto *pExtraMenuBehavior = static_cast<CTouchControls::CExtraMenuTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1287 m_CachedExtraMenuNumber = pExtraMenuBehavior->GetNumber();
1288 }
1289 }
1290 else // Empty
1291 dbg_assert_failed("Detected out of bound value in m_EditBehaviorType");
1292 }
1293}
1294
1295// Will override everything in the button. If nullptr is passed, a new button will be created.
1296void CMenusIngameTouchControls::SaveCachedSettingsToTarget(CTouchControls::CTouchButton *pTargetButton) const
1297{
1298 dbg_assert(pTargetButton != nullptr, "Target button to receive is nullptr.");
1299 pTargetButton->m_UnitRect.m_W = std::clamp(val: m_InputW.GetInteger(), lo: CTouchControls::BUTTON_SIZE_MINIMUM, hi: CTouchControls::BUTTON_SIZE_MAXIMUM);
1300 pTargetButton->m_UnitRect.m_H = std::clamp(val: m_InputH.GetInteger(), lo: CTouchControls::BUTTON_SIZE_MINIMUM, hi: CTouchControls::BUTTON_SIZE_MAXIMUM);
1301 pTargetButton->m_UnitRect.m_X = std::clamp(val: m_InputX.GetInteger(), lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE - pTargetButton->m_UnitRect.m_W);
1302 pTargetButton->m_UnitRect.m_Y = std::clamp(val: m_InputY.GetInteger(), lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE - pTargetButton->m_UnitRect.m_H);
1303 pTargetButton->m_vVisibilities.clear();
1304 for(unsigned Iterator = (unsigned)CTouchControls::EButtonVisibility::INGAME; Iterator < (unsigned)CTouchControls::EButtonVisibility::NUM_VISIBILITIES; ++Iterator)
1305 {
1306 if(m_aCachedVisibilities[Iterator] != EVisibilityType::IGNORE)
1307 pTargetButton->m_vVisibilities.emplace_back(args: (CTouchControls::EButtonVisibility)Iterator, args: m_aCachedVisibilities[Iterator] == EVisibilityType::INCLUDE);
1308 }
1309 pTargetButton->m_Shape = m_CachedShape;
1310 pTargetButton->UpdateScreenFromUnitRect();
1311
1312 // Make a new behavior class instead of modify the original one.
1313 if(m_EditBehaviorType == EBehaviorType::BIND)
1314 {
1315 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CBindTouchButtonBehavior>(
1316 args: m_vBehaviorElements[0]->m_CachedCommands.m_Label.c_str(),
1317 args&: m_vBehaviorElements[0]->m_CachedCommands.m_LabelType,
1318 args: m_vBehaviorElements[0]->m_CachedCommands.m_Command.c_str());
1319 }
1320 else if(m_EditBehaviorType == EBehaviorType::BIND_TOGGLE)
1321 {
1322 std::vector<CTouchControls::CBindToggleTouchButtonBehavior::CCommand> vMovingBehavior;
1323 vMovingBehavior.reserve(n: m_vBehaviorElements.size());
1324 for(const auto &Element : m_vBehaviorElements)
1325 vMovingBehavior.emplace_back(args&: Element->m_CachedCommands);
1326 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CBindToggleTouchButtonBehavior>(args: std::move(vMovingBehavior));
1327 }
1328 else if(m_EditBehaviorType == EBehaviorType::PREDEFINED)
1329 {
1330 if(m_PredefinedBehaviorType == EPredefinedType::EXTRA_MENU)
1331 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CExtraMenuTouchButtonBehavior>(args: CTouchControls::CExtraMenuTouchButtonBehavior(m_CachedExtraMenuNumber));
1332 else
1333 pTargetButton->m_pBehavior = BEHAVIOR_FACTORIES_EDITOR[(int)m_PredefinedBehaviorType].m_Factory();
1334 }
1335 else
1336 {
1337 dbg_assert_failed("Invalid m_EditBehaviorType: %d", static_cast<int>(m_EditBehaviorType));
1338 }
1339 pTargetButton->UpdatePointers();
1340}
1341
1342// Used for setting the value of four position input box to the unitrect.
1343void CMenusIngameTouchControls::SetPosInputs(CTouchControls::CUnitRect MyRect)
1344{
1345 m_InputX.SetInteger(Number: MyRect.m_X);
1346 m_InputY.SetInteger(Number: MyRect.m_Y);
1347 m_InputW.SetInteger(Number: MyRect.m_W);
1348 m_InputH.SetInteger(Number: MyRect.m_H);
1349}
1350
1351CTouchControls::CUnitRect CMenusIngameTouchControls::GetPosInputs() const
1352{
1353 return {.m_X: m_InputX.GetInteger(), .m_Y: m_InputY.GetInteger(), .m_W: m_InputW.GetInteger(), .m_H: m_InputH.GetInteger()};
1354}
1355
1356// Used to make sure the input box is numbers only, also clamp the value.
1357void CMenusIngameTouchControls::InputPosFunction(CLineInputNumber *pInput)
1358{
1359 int InputValue = pInput->GetInteger();
1360 // Deal with the "-1" FindPositionXY give.
1361 InputValue = std::clamp(val: InputValue, lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE);
1362 pInput->SetInteger(Number: InputValue);
1363 SetUnsavedChanges(true);
1364}
1365
1366// Update m_pSampleButton in CTouchControls. The Samplebutton is used for showing on screen.
1367void CMenusIngameTouchControls::UpdateSampleButton()
1368{
1369 GameClient()->m_TouchControls.RemakeSampleButton();
1370 SaveCachedSettingsToTarget(pTargetButton: GameClient()->m_TouchControls.SampleButton());
1371 GameClient()->m_TouchControls.SetShownRect(GameClient()->m_TouchControls.SampleButton()->m_UnitRect);
1372}
1373
1374// Not inline so there's no more includes in menus.h. A shortcut to the function in CTouchControls.
1375void CMenusIngameTouchControls::ResetButtonPointers()
1376{
1377 GameClient()->m_TouchControls.ResetButtonPointers();
1378}
1379
1380// Create a new button with default settings and deal with position issues.
1381bool CMenusIngameTouchControls::NewButton(CTouchControls::CUnitRect Rect, CTouchControls::EButtonShape Shape)
1382{
1383 auto FreeRect = GameClient()->m_TouchControls.UpdatePosition(MyRect: Rect, Shape, Ignore: true);
1384 if(!FreeRect.has_value())
1385 {
1386 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "No space for button"), pMessage: Localize(pStr: "There is not enough space available to place another button."), pButtonLabel: Localize(pStr: "Ok"));
1387 return false;
1388 }
1389 m_pNewSelectedButton = GameClient()->m_TouchControls.NewButton();
1390 GameClient()->m_TouchControls.SetSelectedButton(m_pNewSelectedButton);
1391 SetPosInputs(FreeRect.value());
1392 return true;
1393}
1394
1395// Used for updating cached settings or something else only when opening the editor, to reduce lag. Issues come from CTouchControls.
1396void CMenusIngameTouchControls::ResolveIssues()
1397{
1398 if(GameClient()->m_TouchControls.AnyIssueNotResolved())
1399 {
1400 std::array<CTouchControls::CIssueParam, (unsigned)CTouchControls::EIssueType::NUM_ISSUES> aIssues = GameClient()->m_TouchControls.Issues();
1401 for(unsigned Current = 0; Current < (unsigned)CTouchControls::EIssueType::NUM_ISSUES; Current++)
1402 {
1403 if(aIssues[Current].m_Resolved == true)
1404 continue;
1405 switch(Current)
1406 {
1407 case(int)CTouchControls::EIssueType::CACHE_SETTINGS: CacheAllSettingsFromTarget(pTargetButton: aIssues[Current].m_pTargetButton); break;
1408 case(int)CTouchControls::EIssueType::SAVE_SETTINGS:
1409 {
1410 if(CheckCachedSettings())
1411 SaveCachedSettingsToTarget(pTargetButton: aIssues[Current].m_pTargetButton);
1412 break;
1413 }
1414 case(int)CTouchControls::EIssueType::CACHE_POSITION: SetPosInputs(aIssues[Current].m_pTargetButton->m_UnitRect); break;
1415 default: dbg_assert_failed("Unknown Issue type: %d", Current);
1416 }
1417 }
1418 }
1419}
1420
1421// Turn predefined behavior strings like "joystick-hook" into integers according to the enum.
1422int CMenusIngameTouchControls::CalculatePredefinedType(const char *pType) const
1423{
1424 int IntegerType = (int)EPredefinedType::INGAME_MENU;
1425 static_assert((int)EPredefinedType::INGAME_MENU == 0, "This should start from the first predefined type");
1426 for(; IntegerType < (int)EPredefinedType::NUM_PREDEFINEDTYPES; IntegerType++)
1427 {
1428 if(str_comp(a: pType, b: BEHAVIOR_FACTORIES_EDITOR[IntegerType].m_pId) == 0)
1429 return IntegerType;
1430 }
1431 dbg_assert_failed("Unknown predefined type %s.", pType == nullptr ? "nullptr" : pType);
1432}
1433
1434std::string CMenusIngameTouchControls::DetermineTouchButtonCommandLabel(CTouchControls::CTouchButton *pButton) const
1435{
1436 const char *pBehaviorType = pButton->m_pBehavior->GetBehaviorType();
1437 if(str_comp(a: pBehaviorType, b: CTouchControls::CBindTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1438 {
1439 return static_cast<CTouchControls::CBindTouchButtonBehavior *>(pButton->m_pBehavior.get())->GetCommand();
1440 }
1441 else if(str_comp(a: pBehaviorType, b: CTouchControls::CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1442 {
1443 const auto *pBehavior = static_cast<CTouchControls::CBindToggleTouchButtonBehavior *>(pButton->m_pBehavior.get());
1444 return pBehavior->GetCommand()[pBehavior->GetActiveCommandIndex()].m_Command;
1445 }
1446 else if(str_comp(a: pBehaviorType, b: CTouchControls::CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1447 {
1448 auto *pTargetBehavior = static_cast<CTouchControls::CPredefinedTouchButtonBehavior *>(pButton->m_pBehavior.get());
1449 const char *pPredefinedType = pTargetBehavior->GetPredefinedType();
1450 const char **apPredefinedNames = PredefinedNames();
1451 std::string Command = apPredefinedNames[CalculatePredefinedType(pType: pPredefinedType)];
1452 if(str_comp(a: pPredefinedType, b: CTouchControls::CExtraMenuTouchButtonBehavior::BEHAVIOR_ID) == 0)
1453 {
1454 const auto *pExtraMenuBehavior = static_cast<CTouchControls::CExtraMenuTouchButtonBehavior *>(pTargetBehavior);
1455 Command.append(s: " ");
1456 Command.append(str: std::to_string(val: pExtraMenuBehavior->GetNumber() + 1));
1457 }
1458 return Command;
1459 }
1460 else
1461 {
1462 dbg_assert_failed("Detected unknown behavior type in CMenusIngameTouchControls::GetCommand()");
1463 }
1464}
1465
1466// Used for making json chars like \n or \uf3ce visible.
1467std::string CMenusIngameTouchControls::CBehaviorElements::ParseLabel(const char *pLabel) const
1468{
1469 json_settings JsonSettings{};
1470 char aError[256];
1471 char aJsonString[1048];
1472 str_format(buffer: aJsonString, buffer_size: sizeof(aJsonString), format: "\"%s\"", pLabel);
1473 json_value *pJsonLabel = json_parse_ex(settings: &JsonSettings, json: aJsonString, length: str_length(str: aJsonString), error: aError);
1474 if(pJsonLabel == nullptr || pJsonLabel->type != json_string)
1475 {
1476 return pLabel;
1477 }
1478 std::string ParsedString = pJsonLabel->u.string.ptr;
1479 json_value_free(pJsonLabel);
1480 return ParsedString;
1481}
1482
1483CMenusIngameTouchControls::CBehaviorElements::CBehaviorElements() noexcept
1484{
1485 Reset();
1486}
1487
1488CMenusIngameTouchControls::CBehaviorElements::CBehaviorElements(CBehaviorElements &&Other) noexcept :
1489 m_InputCommand(std::move(Other.m_InputCommand)), m_InputLabel(std::move(Other.m_InputLabel)), m_CachedCommands(std::move(Other.m_CachedCommands))
1490{
1491}
1492
1493CMenusIngameTouchControls::CBehaviorElements::~CBehaviorElements()
1494{
1495 m_InputCommand.Deactivate();
1496 m_InputLabel.Deactivate();
1497}
1498
1499CMenusIngameTouchControls::CBehaviorElements &CMenusIngameTouchControls::CBehaviorElements::operator=(CBehaviorElements &&Other) noexcept
1500{
1501 if(this == &Other)
1502 {
1503 return *this;
1504 }
1505 m_InputCommand = std::move(Other.m_InputCommand);
1506 m_InputLabel = std::move(Other.m_InputLabel);
1507 m_CachedCommands = std::move(Other.m_CachedCommands);
1508 return *this;
1509}
1510
1511void CMenusIngameTouchControls::CBehaviorElements::UpdateInputs()
1512{
1513 m_InputLabel.Set(ParseLabel(pLabel: m_CachedCommands.m_Label.c_str()).c_str());
1514 m_InputCommand.Set(m_CachedCommands.m_Command.c_str());
1515}
1516
1517void CMenusIngameTouchControls::CBehaviorElements::Reset()
1518{
1519 m_CachedCommands = {};
1520 m_InputCommand.Clear();
1521 m_InputLabel.Clear();
1522}
1523
1524const char **CMenusIngameTouchControls::VisibilityNames() const
1525{
1526 static const char *s_apVisibilities[8];
1527 s_apVisibilities[0] = Localize(pStr: "Ingame", pContext: "Touch button visibilities");
1528 s_apVisibilities[1] = Localize(pStr: "Zoom Allowed", pContext: "Touch button visibilities");
1529 s_apVisibilities[2] = Localize(pStr: "Vote Active", pContext: "Touch button visibilities");
1530 s_apVisibilities[3] = Localize(pStr: "Dummy Allowed", pContext: "Touch button visibilities");
1531 s_apVisibilities[4] = Localize(pStr: "Dummy Connected", pContext: "Touch button visibilities");
1532 s_apVisibilities[5] = Localize(pStr: "Rcon Authed", pContext: "Touch button visibilities");
1533 s_apVisibilities[6] = Localize(pStr: "Demo Player", pContext: "Touch button visibilities");
1534 s_apVisibilities[7] = Localize(pStr: "Extra Menu", pContext: "Touch button visibilities");
1535 static_assert(std::size(s_apVisibilities) == (size_t)CTouchControls::EButtonVisibility::NUM_VISIBILITIES - CTouchControls::MAX_EXTRA_MENU_NUMBER + 1, "Insufficient visibility names");
1536 return s_apVisibilities;
1537}
1538
1539const char **CMenusIngameTouchControls::PredefinedNames() const
1540{
1541 static const char *s_apPredefined[10];
1542 s_apPredefined[0] = Localize(pStr: "Ingame Menu", pContext: "Predefined touch button behaviors");
1543 s_apPredefined[1] = Localize(pStr: "Extra Menu", pContext: "Predefined touch button behaviors");
1544 s_apPredefined[2] = Localize(pStr: "Emoticon", pContext: "Predefined touch button behaviors");
1545 s_apPredefined[3] = Localize(pStr: "Spectate", pContext: "Predefined touch button behaviors");
1546 s_apPredefined[4] = Localize(pStr: "Swap Action", pContext: "Predefined touch button behaviors");
1547 s_apPredefined[5] = Localize(pStr: "Use Action", pContext: "Predefined touch button behaviors");
1548 s_apPredefined[6] = Localize(pStr: "Joystick Action", pContext: "Predefined touch button behaviors");
1549 s_apPredefined[7] = Localize(pStr: "Joystick Aim", pContext: "Predefined touch button behaviors");
1550 s_apPredefined[8] = Localize(pStr: "Joystick Fire", pContext: "Predefined touch button behaviors");
1551 s_apPredefined[9] = Localize(pStr: "Joystick Hook", pContext: "Predefined touch button behaviors");
1552 static_assert(std::size(s_apPredefined) == std::size(BEHAVIOR_FACTORIES_EDITOR), "Insufficient predefined names");
1553 return s_apPredefined;
1554}
1555
1556const char *CMenusIngameTouchControls::HelpMessageForBehaviorType(EBehaviorType Type) const
1557{
1558 switch(m_EditBehaviorType)
1559 {
1560 case EBehaviorType::BIND: return Localize(pStr: "Buttons with this behavior execute console commands like regular key binds."); break;
1561 case EBehaviorType::BIND_TOGGLE: return Localize(pStr: "Buttons with this behavior cycle between executing one of two or more specified commands."); break;
1562 case EBehaviorType::PREDEFINED: return Localize(pStr: "Buttons with this behavior execute some predefined actions."); break;
1563 default: dbg_assert_failed("Unknown behavior %d", (int)m_EditBehaviorType);
1564 }
1565 static_assert((int)EBehaviorType::NUM_BEHAVIORS == 3, "Insufficient help messages");
1566}
1567
1568const char *CMenusIngameTouchControls::HelpMessageForPredefinedType(EPredefinedType Type) const
1569{
1570 switch(m_PredefinedBehaviorType)
1571 {
1572 case EPredefinedType::INGAME_MENU: return Localize(pStr: "Opens the ingame menu immediately when released."); break;
1573 case EPredefinedType::EXTRA_MENU: return Localize(pStr: "The extra menu button which toggles visibility of buttons with \"Extra Menu\" visibilities. Also opens the ingame menu on long press."); break;
1574 case EPredefinedType::EMOTICON: return Localize(pStr: "Opens the emoticon selector (this does not work with binds)."); break;
1575 case EPredefinedType::SPECTATE: return Localize(pStr: "Opens the spectator menu (this does not work with binds)."); break;
1576 case EPredefinedType::SWAP_ACTION: return Localize(pStr: "Swaps the active action (fire and hook) for direct touch input and virtual joysticks."); break;
1577 case EPredefinedType::USE_ACTION: return Localize(pStr: "Uses the active action with the current aiming position."); break;
1578 case EPredefinedType::JOYSTICK_ACTION: return Localize(pStr: "Virtual joystick which uses the active action."); break;
1579 case EPredefinedType::JOYSTICK_AIM: return Localize(pStr: "Virtual joystick which only aims without using an action."); break;
1580 case EPredefinedType::JOYSTICK_FIRE: return Localize(pStr: "Virtual joystick which always uses fire."); break;
1581 case EPredefinedType::JOYSTICK_HOOK: return Localize(pStr: "Virtual joystick which always uses hook."); break;
1582 default: dbg_assert_failed("Unknown behavior %d", (int)m_PredefinedBehaviorType);
1583 }
1584 static_assert((int)EPredefinedType::NUM_PREDEFINEDTYPES == 10, "Insufficient help messages");
1585}
1586
1587const char *CMenusIngameTouchControls::HelpMessageForVisibilityType(CTouchControls::EButtonVisibility Type) const
1588{
1589 switch(Type)
1590 {
1591 case CTouchControls::EButtonVisibility::INGAME: return Localize(pStr: "Player is ingame, i.e. not spectating."); break;
1592 case CTouchControls::EButtonVisibility::ZOOM_ALLOWED: return Localize(pStr: "Zoom is allowed on this server."); break;
1593 case CTouchControls::EButtonVisibility::VOTE_ACTIVE: return Localize(pStr: "A vote is currently active."); break;
1594 case CTouchControls::EButtonVisibility::DUMMY_ALLOWED: return Localize(pStr: "Dummy is allowed on this server."); break;
1595 case CTouchControls::EButtonVisibility::DUMMY_CONNECTED: return Localize(pStr: "Dummy is currently connected."); break;
1596 case CTouchControls::EButtonVisibility::RCON_AUTHED: return Localize(pStr: "Player is currently authed in rcon."); break;
1597 case CTouchControls::EButtonVisibility::DEMO_PLAYER: return Localize(pStr: "Demo player is currently active."); break;
1598 case CTouchControls::EButtonVisibility::EXTRA_MENU_1: return Localize(pStr: "The extra menu with the given number is activated."); break;
1599 default: dbg_assert_failed("Unknown visibility type %d", (int)Type);
1600 }
1601 static_assert((int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES - CTouchControls::MAX_EXTRA_MENU_NUMBER + 1 == 8, "Insufficient help messages");
1602}
1603