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