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