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