1#include "menus_ingame_touch_controls.h"
2
3#include <base/color.h>
4#include <base/dbg.h>
5#include <base/str.h>
6
7#include <engine/external/json-parser/json.h>
8#include <engine/font_icons.h>
9#include <engine/graphics.h>
10#include <engine/shared/json.h>
11#include <engine/shared/jsonwriter.h>
12#include <engine/shared/localization.h>
13#include <engine/textrender.h>
14
15#include <game/client/components/touch_controls.h>
16#include <game/client/gameclient.h>
17#include <game/client/lineinput.h>
18#include <game/client/ui.h>
19#include <game/client/ui_listbox.h>
20#include <game/client/ui_rect.h>
21#include <game/client/ui_scrollregion.h>
22#include <game/localization.h>
23
24#include <algorithm>
25#include <memory>
26#include <string>
27
28static const constexpr float MAINMARGIN = 10.0f;
29static const constexpr float SUBMARGIN = 5.0f;
30static const constexpr float ROWSIZE = 25.0f;
31static const constexpr float ROWGAP = 5.0f;
32static const constexpr float TITLESIZE = 20.0f;
33static const constexpr float FONTSIZE = 15.0f;
34
35const CMenusIngameTouchControls::CBehaviorFactoryEditor CMenusIngameTouchControls::BEHAVIOR_FACTORIES_EDITOR[] = {
36 {.m_pId: CTouchControls::CIngameMenuTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CIngameMenuTouchButtonBehavior>(); }},
37 {.m_pId: CTouchControls::CExtraMenuTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CExtraMenuTouchButtonBehavior>(args: 0); }},
38 {.m_pId: CTouchControls::CEmoticonTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CEmoticonTouchButtonBehavior>(); }},
39 {.m_pId: CTouchControls::CSpectateTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CSpectateTouchButtonBehavior>(); }},
40 {.m_pId: CTouchControls::CSwapActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CSwapActionTouchButtonBehavior>(); }},
41 {.m_pId: CTouchControls::CUseActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CUseActionTouchButtonBehavior>(); }},
42 {.m_pId: CTouchControls::CJoystickActionTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickActionTouchButtonBehavior>(); }},
43 {.m_pId: CTouchControls::CJoystickAimTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickAimTouchButtonBehavior>(); }},
44 {.m_pId: CTouchControls::CJoystickAimRelativeTouchButtonBehavior::BEHAVIOR_ID, .m_Factory: []() { return std::make_unique<CTouchControls::CJoystickAimRelativeTouchButtonBehavior>(); }},
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_ScrollbarThickness / 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_ScrollbarThickness / 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_ScrollbarThickness / 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_ScrollbarThickness / 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_ScrollbarThickness / 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] = {
949 Localize(pStr: "Disabled", pContext: "Direct touch input"),
950 Localize(pStr: "Active action", pContext: "Direct touch input"),
951 Localize(pStr: "Aim", pContext: "Direct touch input"),
952 Localize(pStr: "Relative aim", pContext: "Direct touch input"),
953 Localize(pStr: "Fire", pContext: "Direct touch input"),
954 Localize(pStr: "Hook", pContext: "Direct touch input"),
955 };
956 const CTouchControls::EDirectTouchIngameMode OldDirectTouchIngame = GameClient()->m_TouchControls.DirectTouchIngame();
957 static CUi::SDropDownState s_DirectTouchIngameDropDownState;
958 static CScrollRegion s_DirectTouchIngameDropDownScrollRegion;
959 s_DirectTouchIngameDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchIngameDropDownScrollRegion;
960 const CTouchControls::EDirectTouchIngameMode NewDirectTouchIngame = (CTouchControls::EDirectTouchIngameMode)Ui()->DoDropDown(pRect: &Button, CurSelection: (int)OldDirectTouchIngame, pStrs: apIngameTouchModes, Num: std::size(apIngameTouchModes), State&: s_DirectTouchIngameDropDownState);
961 if(OldDirectTouchIngame != NewDirectTouchIngame)
962 {
963 GameClient()->m_TouchControls.SetDirectTouchIngame(NewDirectTouchIngame);
964 }
965
966 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
967 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
968 Row.VSplitMid(pLeft: &Label, pRight: &Button);
969 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Direct touch input while spectating"), Size: FONTSIZE, Align: TEXTALIGN_ML);
970
971 const char *apSpectateTouchModes[(int)CTouchControls::EDirectTouchSpectateMode::NUM_STATES] = {Localize(pStr: "Disabled", pContext: "Direct touch input"), Localize(pStr: "Aim", pContext: "Direct touch input")};
972 const CTouchControls::EDirectTouchSpectateMode OldDirectTouchSpectate = GameClient()->m_TouchControls.DirectTouchSpectate();
973 static CUi::SDropDownState s_DirectTouchSpectateDropDownState;
974 static CScrollRegion s_DirectTouchSpectateDropDownScrollRegion;
975 s_DirectTouchSpectateDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_DirectTouchSpectateDropDownScrollRegion;
976 const CTouchControls::EDirectTouchSpectateMode NewDirectTouchSpectate = (CTouchControls::EDirectTouchSpectateMode)Ui()->DoDropDown(pRect: &Button, CurSelection: (int)OldDirectTouchSpectate, pStrs: apSpectateTouchModes, Num: std::size(apSpectateTouchModes), State&: s_DirectTouchSpectateDropDownState);
977 if(OldDirectTouchSpectate != NewDirectTouchSpectate)
978 {
979 GameClient()->m_TouchControls.SetDirectTouchSpectate(NewDirectTouchSpectate);
980 }
981}
982
983void CMenusIngameTouchControls::RenderPreviewSettings(CUIRect MainView)
984{
985 CUIRect EditBox, Button;
986 MainView.h = 600.0f - 40.0f - MainView.y;
987 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
988 MainView.Margin(Cut: MAINMARGIN, pOtherRect: &MainView);
989 MainView.HSplitTop(Cut: ROWSIZE, pTop: &EditBox, pBottom: &MainView);
990 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
991 EditBox.VSplitLeft(Cut: EditBox.h, pLeft: nullptr, pRight: &EditBox);
992 EditBox.VSplitRight(Cut: EditBox.h, pLeft: &EditBox, pRight: &Button);
993 EditBox.VMargin(Cut: 5.0f, pOtherRect: &EditBox);
994 Ui()->DoLabel(pRect: &EditBox, pText: Localize(pStr: "Preview visibilities"), Size: TITLESIZE, Align: TEXTALIGN_MC);
995 static CButtonContainer s_OpenHelpButton;
996 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
997 {
998 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Info"), pMessage: Localize(pStr: "Preview button visibility while the editor is active."), pButtonLabel: Localize(pStr: "Ok"));
999 }
1000
1001 MainView.HSplitBottom(Cut: ROWSIZE, pTop: &MainView, pBottom: &EditBox);
1002 MainView.HSplitBottom(Cut: ROWGAP, pTop: &MainView, pBottom: nullptr);
1003 EditBox.VSplitLeft(Cut: MAINMARGIN, pLeft: nullptr, pRight: &EditBox);
1004 static CButtonContainer s_PreviewAllCheckBox;
1005 bool Preview = GameClient()->m_TouchControls.PreviewAllButtons();
1006 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &s_PreviewAllCheckBox, pText: Localize(pStr: "Show all buttons"), Checked: Preview ? 1 : 0, pRect: &EditBox))
1007 {
1008 GameClient()->m_TouchControls.SetPreviewAllButtons(!Preview);
1009 }
1010 MainView.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1011 MainView.VMargin(Cut: MAINMARGIN, pOtherRect: &MainView);
1012 MainView.HMargin(Cut: ROWGAP, pOtherRect: &MainView);
1013 static CScrollRegion s_VirtualVisibilityScrollRegion;
1014 CScrollRegionParams ScrollParam;
1015 ScrollParam.m_ScrollUnit = 90.0f;
1016 s_VirtualVisibilityScrollRegion.Begin(pClipRect: &MainView, pParams: &ScrollParam);
1017 std::array<bool, (size_t)CTouchControls::EButtonVisibility::NUM_VISIBILITIES> aVirtualVisibilities = GameClient()->m_TouchControls.VirtualVisibilities();
1018 const char **ppVisibilities = VisibilityNames();
1019 for(unsigned Current = 0; Current < (unsigned)CTouchControls::EButtonVisibility::NUM_VISIBILITIES; ++Current)
1020 {
1021 MainView.HSplitTop(Cut: ROWSIZE + ROWGAP, pTop: &EditBox, pBottom: &MainView);
1022 if(s_VirtualVisibilityScrollRegion.AddRect(Rect: EditBox))
1023 {
1024 EditBox.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &EditBox);
1025 if(Current < (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1)
1026 {
1027 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aVisibilityIds[Current], pText: ppVisibilities[Current], Checked: aVirtualVisibilities[Current] == 1, pRect: &EditBox))
1028 GameClient()->m_TouchControls.ReverseVirtualVisibilities(Number: Current);
1029 }
1030 else
1031 {
1032 unsigned ExtraMenuNumber = Current - (unsigned)CTouchControls::EButtonVisibility::EXTRA_MENU_1 + 1;
1033 char aBuf[64];
1034 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d", ppVisibilities[(int)CTouchControls::EButtonVisibility::EXTRA_MENU_1], ExtraMenuNumber);
1035 if(GameClient()->m_Menus.DoButton_CheckBox(pId: &m_aVisibilityIds[Current], pText: aBuf, Checked: aVirtualVisibilities[Current] == 1, pRect: &EditBox))
1036 GameClient()->m_TouchControls.ReverseVirtualVisibilities(Number: Current);
1037 }
1038 }
1039 }
1040 s_VirtualVisibilityScrollRegion.End();
1041}
1042
1043void CMenusIngameTouchControls::RenderTouchControlsEditor(CUIRect MainView)
1044{
1045 CUIRect Label, Button, Row;
1046 MainView.h = 2 * MAINMARGIN + 4 * ROWSIZE + 3 * ROWGAP;
1047 MainView.Draw(Color: CMenus::ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1048 MainView.Margin(Cut: MAINMARGIN, pOtherRect: &MainView);
1049
1050 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1051 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1052 Row.VSplitLeft(Cut: Row.h, pLeft: nullptr, pRight: &Row);
1053 Row.VSplitRight(Cut: Row.h, pLeft: &Row, pRight: &Button);
1054 Row.VMargin(Cut: 5.0f, pOtherRect: &Label);
1055 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Edit touch controls"), Size: TITLESIZE, Align: TEXTALIGN_MC);
1056
1057 static CButtonContainer s_OpenHelpButton;
1058 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OpenHelpButton, pText: FontIcon::QUESTION, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
1059 {
1060 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Info"),
1061 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."),
1062 pConfirmButtonLabel: Localize(pStr: "Open DDNet Wiki"), pCancelButtonLabel: Localize(pStr: "Ok"), pfnConfirmButtonCallback: &CMenus::PopupConfirmOpenWiki);
1063 }
1064
1065 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1066 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1067
1068 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1069 static CButtonContainer s_SaveConfigurationButton;
1070 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_SaveConfigurationButton, pText: Localize(pStr: "Save changes"), Checked: GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, pRect: &Button))
1071 {
1072 if(GameClient()->m_TouchControls.SaveConfigurationToFile())
1073 {
1074 GameClient()->m_TouchControls.SetEditingChanges(false);
1075 }
1076 else
1077 {
1078 SWarning Warning(Localize(pStr: "Error saving touch controls"), Localize(pStr: "Could not save touch controls to file. See local console for details."));
1079 Warning.m_AutoHide = false;
1080 Client()->AddWarning(Warning);
1081 }
1082 }
1083
1084 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1085 if(GameClient()->m_TouchControls.HasEditingChanges())
1086 {
1087 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
1088 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Unsaved changes"), Size: 14.0f, Align: TEXTALIGN_MC);
1089 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1090 }
1091
1092 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1093 MainView.HSplitTop(Cut: ROWGAP, pTop: nullptr, pBottom: &MainView);
1094
1095 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1096 static CButtonContainer s_DiscardChangesButton;
1097 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_DiscardChangesButton, pText: Localize(pStr: "Discard changes"), Checked: GameClient()->m_TouchControls.HasEditingChanges() ? 0 : 1, pRect: &Button))
1098 {
1099 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Discard changes"),
1100 pMessage: Localize(pStr: "Are you sure that you want to discard the current changes to the touch controls?"),
1101 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1102 pfnConfirmButtonCallback: &CMenus::PopupConfirmDiscardTouchControlsChanges);
1103 }
1104
1105 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1106 static CButtonContainer s_ResetButton;
1107 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &Button))
1108 {
1109 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Reset to defaults"),
1110 pMessage: Localize(pStr: "Are you sure that you want to reset the touch controls to default?"),
1111 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1112 pfnConfirmButtonCallback: &CMenus::PopupConfirmResetTouchControls);
1113 }
1114
1115 MainView.HSplitTop(Cut: ROWSIZE, pTop: &Row, pBottom: &MainView);
1116 MainView.HSplitTop(Cut: MAINMARGIN, pTop: nullptr, pBottom: &MainView);
1117
1118 Row.VSplitLeft(Cut: (Row.w - SUBMARGIN) / 2.0f, pLeft: &Button, pRight: &Row);
1119 static CButtonContainer s_ClipboardImportButton;
1120 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ClipboardImportButton, pText: Localize(pStr: "Import from clipboard"), Checked: 0, pRect: &Button))
1121 {
1122 GameClient()->m_Menus.PopupConfirm(pTitle: Localize(pStr: "Import from clipboard"),
1123 pMessage: Localize(pStr: "Are you sure that you want to import the touch controls from the clipboard? This will overwrite your current touch controls."),
1124 pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"),
1125 pfnConfirmButtonCallback: &CMenus::PopupConfirmImportTouchControlsClipboard);
1126 }
1127
1128 Row.VSplitLeft(Cut: SUBMARGIN, pLeft: nullptr, pRight: &Button);
1129 static CButtonContainer s_ClipboardExportButton;
1130 if(GameClient()->m_Menus.DoButton_Menu(pButtonContainer: &s_ClipboardExportButton, pText: Localize(pStr: "Export to clipboard"), Checked: 0, pRect: &Button))
1131 {
1132 GameClient()->m_TouchControls.SaveConfigurationToClipboard();
1133 }
1134}
1135
1136// Check if CTouchControls need CMenus to open any popups.
1137void CMenusIngameTouchControls::DoPopupType(CTouchControls::CPopupParam PopupParam)
1138{
1139 m_pOldSelectedButton = PopupParam.m_pOldSelectedButton;
1140 m_pNewSelectedButton = PopupParam.m_pNewSelectedButton;
1141 m_CloseMenu = !PopupParam.m_KeepMenuOpen;
1142 switch(PopupParam.m_PopupType)
1143 {
1144 case CTouchControls::EPopupType::BUTTON_CHANGED: ChangeSelectedButtonWhileHavingUnsavedChanges(); break;
1145 case CTouchControls::EPopupType::NO_SPACE: NoSpaceForOverlappingButton(); break;
1146 case CTouchControls::EPopupType::BUTTON_INVISIBLE: SelectedButtonNotVisible(); break;
1147 // The NUM_POPUPS will not call the function.
1148 default: dbg_assert_failed("Unknown popup type = %d.", (int)PopupParam.m_PopupType);
1149 }
1150}
1151
1152void CMenusIngameTouchControls::ChangeSelectedButtonWhileHavingUnsavedChanges() const
1153{
1154 // Both old and new button pointer can be nullptr.
1155 // Saving settings to the old selected button(nullptr = create), then switch to new selected button(new = haven't created).
1156 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);
1157}
1158
1159void CMenusIngameTouchControls::NoSpaceForOverlappingButton() const
1160{
1161 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"));
1162}
1163
1164void CMenusIngameTouchControls::SelectedButtonNotVisible()
1165{
1166 // Cancel shouldn't do anything but open ingame menu, the menu is already opened now.
1167 m_CloseMenu = false;
1168 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);
1169}
1170
1171bool CMenusIngameTouchControls::UnsavedChanges() const
1172{
1173 return GameClient()->m_TouchControls.HasUnsavedChanges();
1174}
1175
1176void CMenusIngameTouchControls::SetUnsavedChanges(bool UnsavedChanges)
1177{
1178 GameClient()->m_TouchControls.SetUnsavedChanges(UnsavedChanges);
1179}
1180
1181// Check if cached settings are legal.
1182bool CMenusIngameTouchControls::CheckCachedSettings() const
1183{
1184 std::vector<const char *> vpErrors;
1185 char aBuf[256];
1186 auto [X, Y, W, H] = GetPosInputs();
1187 // Illegal size settings.
1188 if(W < CTouchControls::BUTTON_SIZE_MINIMUM || W > CTouchControls::BUTTON_SIZE_MAXIMUM || H < CTouchControls::BUTTON_SIZE_MINIMUM || H > CTouchControls::BUTTON_SIZE_MAXIMUM)
1189 {
1190 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);
1191 vpErrors.emplace_back(args&: aBuf);
1192 }
1193 if(X < 0 || Y < 0 || X + W > CTouchControls::BUTTON_SIZE_SCALE || Y + H > CTouchControls::BUTTON_SIZE_SCALE)
1194 {
1195 vpErrors.emplace_back(args: Localize(pStr: "Button position is outside of the screen."));
1196 }
1197 if(GameClient()->m_TouchControls.IsRectOverlapping(MyRect: {.m_X: X, .m_Y: Y, .m_W: W, .m_H: H}, Shape: m_CachedShape))
1198 {
1199 vpErrors.emplace_back(args: Localize(pStr: "The selected button is overlapping with other buttons."));
1200 }
1201 if(!vpErrors.empty())
1202 {
1203 char aErrorMessage[1024] = "";
1204 for(const char *pError : vpErrors)
1205 {
1206 if(aErrorMessage[0] != '\0')
1207 str_append(dst&: aErrorMessage, src: "\n");
1208 str_append(dst&: aErrorMessage, src: pError);
1209 }
1210 GameClient()->m_Menus.PopupMessage(pTitle: Localize(pStr: "Wrong button settings"), pMessage: aErrorMessage, pButtonLabel: Localize(pStr: "Ok"));
1211 return false;
1212 }
1213 else
1214 {
1215 return true;
1216 }
1217}
1218
1219// All default settings are here.
1220void CMenusIngameTouchControls::ResetCachedSettings()
1221{
1222 // Reset all cached values.
1223 m_EditBehaviorType = EBehaviorType::BIND;
1224 m_PredefinedBehaviorType = EPredefinedType::EXTRA_MENU;
1225 m_CachedExtraMenuNumber = 0;
1226 m_vBehaviorElements.clear();
1227 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1228 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1229 m_aCachedVisibilities.fill(u: EVisibilityType::IGNORE); // 2 means don't have the visibility, true:1,false:0
1230 m_aCachedVisibilities[(int)CTouchControls::EButtonVisibility::DEMO_PLAYER] = EVisibilityType::EXCLUDE;
1231 // These things can't be empty. std::stoi can't cast empty string.
1232 SetPosInputs({.m_X: 0, .m_Y: 0, .m_W: CTouchControls::BUTTON_SIZE_MINIMUM, .m_H: CTouchControls::BUTTON_SIZE_MINIMUM});
1233 m_CachedShape = CTouchControls::EButtonShape::RECT;
1234}
1235
1236// This is called when the Touch button editor is rendered as well when selectedbutton changes. Used for updating all cached settings.
1237void CMenusIngameTouchControls::CacheAllSettingsFromTarget(CTouchControls::CTouchButton *pTargetButton)
1238{
1239 ResetCachedSettings();
1240 if(pTargetButton == nullptr)
1241 {
1242 return; // Nothing to cache.
1243 }
1244 // These values can't be null. The constructor has been updated. Default:{0,0,CTouchControls::BUTTON_SIZE_MINIMUM,CTouchControls::BUTTON_SIZE_MINIMUM}, shape = rect.
1245 SetPosInputs(pTargetButton->m_UnitRect);
1246 m_CachedShape = pTargetButton->m_Shape;
1247 for(const auto &Visibility : pTargetButton->m_vVisibilities)
1248 {
1249 dbg_assert((int)Visibility.m_Type < (int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES, "Target button has out of bound visibility type: %d", (int)Visibility.m_Type);
1250 m_aCachedVisibilities[(int)Visibility.m_Type] = Visibility.m_Parity ? EVisibilityType::INCLUDE : EVisibilityType::EXCLUDE;
1251 }
1252
1253 // These are behavior values.
1254 if(pTargetButton->m_pBehavior != nullptr)
1255 {
1256 const char *pBehaviorType = pTargetButton->m_pBehavior->GetBehaviorType();
1257 if(str_comp(a: pBehaviorType, b: CTouchControls::CBindTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1258 {
1259 m_EditBehaviorType = EBehaviorType::BIND;
1260 auto *pTargetBehavior = static_cast<CTouchControls::CBindTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1261 // m_LabelType must not be null. Default value is PLAIN.
1262 m_vBehaviorElements[0]->m_CachedCommands = {pTargetBehavior->GetLabel().m_pLabel, pTargetBehavior->GetLabel().m_Type, pTargetBehavior->GetCommand()};
1263 m_vBehaviorElements[0]->UpdateInputs();
1264 }
1265 else if(str_comp(a: pBehaviorType, b: CTouchControls::CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1266 {
1267 m_EditBehaviorType = EBehaviorType::BIND_TOGGLE;
1268 auto *pTargetBehavior = static_cast<CTouchControls::CBindToggleTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1269 auto TargetCommands = pTargetBehavior->GetCommand();
1270 // Can't use resize here :(
1271 while(m_vBehaviorElements.size() > std::max(a: TargetCommands.size(), b: (size_t)2))
1272 m_vBehaviorElements.pop_back();
1273 while(m_vBehaviorElements.size() < std::max(a: TargetCommands.size(), b: (size_t)2))
1274 m_vBehaviorElements.emplace_back(args: std::make_unique<CBehaviorElements>());
1275 for(unsigned CommandIndex = 0; CommandIndex < TargetCommands.size(); CommandIndex++)
1276 {
1277 m_vBehaviorElements[CommandIndex]->m_CachedCommands = TargetCommands[CommandIndex];
1278 m_vBehaviorElements[CommandIndex]->UpdateInputs();
1279 }
1280 }
1281 else if(str_comp(a: pBehaviorType, b: CTouchControls::CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1282 {
1283 m_EditBehaviorType = EBehaviorType::PREDEFINED;
1284 auto *pTargetBehavior = static_cast<CTouchControls::CPredefinedTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1285 const char *pPredefinedType = pTargetBehavior->GetPredefinedType();
1286 if(pPredefinedType == nullptr)
1287 m_PredefinedBehaviorType = EPredefinedType::EXTRA_MENU;
1288 else
1289 m_PredefinedBehaviorType = (EPredefinedType)CalculatePredefinedType(pType: pPredefinedType);
1290 dbg_assert(m_PredefinedBehaviorType != EPredefinedType::NUM_PREDEFINEDTYPES, "Detected out of bound m_PredefinedBehaviorType. pPredefinedType = %s", pPredefinedType);
1291
1292 if(m_PredefinedBehaviorType == EPredefinedType::EXTRA_MENU)
1293 {
1294 auto *pExtraMenuBehavior = static_cast<CTouchControls::CExtraMenuTouchButtonBehavior *>(pTargetButton->m_pBehavior.get());
1295 m_CachedExtraMenuNumber = pExtraMenuBehavior->GetNumber();
1296 }
1297 }
1298 else // Empty
1299 dbg_assert_failed("Detected out of bound value in m_EditBehaviorType");
1300 }
1301}
1302
1303// Will override everything in the button. If nullptr is passed, a new button will be created.
1304void CMenusIngameTouchControls::SaveCachedSettingsToTarget(CTouchControls::CTouchButton *pTargetButton) const
1305{
1306 dbg_assert(pTargetButton != nullptr, "Target button to receive is nullptr.");
1307 pTargetButton->m_UnitRect.m_W = std::clamp(val: m_InputW.GetInteger(), lo: CTouchControls::BUTTON_SIZE_MINIMUM, hi: CTouchControls::BUTTON_SIZE_MAXIMUM);
1308 pTargetButton->m_UnitRect.m_H = std::clamp(val: m_InputH.GetInteger(), lo: CTouchControls::BUTTON_SIZE_MINIMUM, hi: CTouchControls::BUTTON_SIZE_MAXIMUM);
1309 pTargetButton->m_UnitRect.m_X = std::clamp(val: m_InputX.GetInteger(), lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE - pTargetButton->m_UnitRect.m_W);
1310 pTargetButton->m_UnitRect.m_Y = std::clamp(val: m_InputY.GetInteger(), lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE - pTargetButton->m_UnitRect.m_H);
1311 pTargetButton->m_vVisibilities.clear();
1312 for(unsigned Iterator = (unsigned)CTouchControls::EButtonVisibility::INGAME; Iterator < (unsigned)CTouchControls::EButtonVisibility::NUM_VISIBILITIES; ++Iterator)
1313 {
1314 if(m_aCachedVisibilities[Iterator] != EVisibilityType::IGNORE)
1315 pTargetButton->m_vVisibilities.emplace_back(args: (CTouchControls::EButtonVisibility)Iterator, args: m_aCachedVisibilities[Iterator] == EVisibilityType::INCLUDE);
1316 }
1317 pTargetButton->m_Shape = m_CachedShape;
1318 pTargetButton->UpdateScreenFromUnitRect();
1319
1320 // Make a new behavior class instead of modify the original one.
1321 if(m_EditBehaviorType == EBehaviorType::BIND)
1322 {
1323 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CBindTouchButtonBehavior>(
1324 args: m_vBehaviorElements[0]->m_CachedCommands.m_Label.c_str(),
1325 args&: m_vBehaviorElements[0]->m_CachedCommands.m_LabelType,
1326 args: m_vBehaviorElements[0]->m_CachedCommands.m_Command.c_str());
1327 }
1328 else if(m_EditBehaviorType == EBehaviorType::BIND_TOGGLE)
1329 {
1330 std::vector<CTouchControls::CBindToggleTouchButtonBehavior::CCommand> vMovingBehavior;
1331 vMovingBehavior.reserve(n: m_vBehaviorElements.size());
1332 for(const auto &Element : m_vBehaviorElements)
1333 vMovingBehavior.emplace_back(args&: Element->m_CachedCommands);
1334 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CBindToggleTouchButtonBehavior>(args: std::move(vMovingBehavior));
1335 }
1336 else if(m_EditBehaviorType == EBehaviorType::PREDEFINED)
1337 {
1338 if(m_PredefinedBehaviorType == EPredefinedType::EXTRA_MENU)
1339 pTargetButton->m_pBehavior = std::make_unique<CTouchControls::CExtraMenuTouchButtonBehavior>(args: CTouchControls::CExtraMenuTouchButtonBehavior(m_CachedExtraMenuNumber));
1340 else
1341 pTargetButton->m_pBehavior = BEHAVIOR_FACTORIES_EDITOR[(int)m_PredefinedBehaviorType].m_Factory();
1342 }
1343 else
1344 {
1345 dbg_assert_failed("Invalid m_EditBehaviorType: %d", static_cast<int>(m_EditBehaviorType));
1346 }
1347 pTargetButton->UpdatePointers();
1348}
1349
1350// Used for setting the value of four position input box to the unitrect.
1351void CMenusIngameTouchControls::SetPosInputs(CTouchControls::CUnitRect MyRect)
1352{
1353 m_InputX.SetInteger(Number: MyRect.m_X);
1354 m_InputY.SetInteger(Number: MyRect.m_Y);
1355 m_InputW.SetInteger(Number: MyRect.m_W);
1356 m_InputH.SetInteger(Number: MyRect.m_H);
1357}
1358
1359CTouchControls::CUnitRect CMenusIngameTouchControls::GetPosInputs() const
1360{
1361 return {.m_X: m_InputX.GetInteger(), .m_Y: m_InputY.GetInteger(), .m_W: m_InputW.GetInteger(), .m_H: m_InputH.GetInteger()};
1362}
1363
1364// Used to make sure the input box is numbers only, also clamp the value.
1365void CMenusIngameTouchControls::InputPosFunction(CLineInputNumber *pInput)
1366{
1367 int InputValue = pInput->GetInteger();
1368 // Deal with the "-1" FindPositionXY give.
1369 InputValue = std::clamp(val: InputValue, lo: 0, hi: CTouchControls::BUTTON_SIZE_SCALE);
1370 pInput->SetInteger(Number: InputValue);
1371 SetUnsavedChanges(true);
1372}
1373
1374// Update m_pSampleButton in CTouchControls. The Samplebutton is used for showing on screen.
1375void CMenusIngameTouchControls::UpdateSampleButton()
1376{
1377 GameClient()->m_TouchControls.RemakeSampleButton();
1378 SaveCachedSettingsToTarget(pTargetButton: GameClient()->m_TouchControls.SampleButton());
1379 GameClient()->m_TouchControls.SetShownRect(GameClient()->m_TouchControls.SampleButton()->m_UnitRect);
1380}
1381
1382// Not inline so there's no more includes in menus.h. A shortcut to the function in CTouchControls.
1383void CMenusIngameTouchControls::ResetButtonPointers()
1384{
1385 GameClient()->m_TouchControls.ResetButtonPointers();
1386}
1387
1388// Create a new button with default settings and deal with position issues.
1389bool CMenusIngameTouchControls::NewButton(CTouchControls::CUnitRect Rect, CTouchControls::EButtonShape Shape)
1390{
1391 auto FreeRect = GameClient()->m_TouchControls.UpdatePosition(MyRect: Rect, Shape, Ignore: true);
1392 if(!FreeRect.has_value())
1393 {
1394 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"));
1395 return false;
1396 }
1397 m_pNewSelectedButton = GameClient()->m_TouchControls.NewButton();
1398 GameClient()->m_TouchControls.SetSelectedButton(m_pNewSelectedButton);
1399 SetPosInputs(FreeRect.value());
1400 return true;
1401}
1402
1403// Used for updating cached settings or something else only when opening the editor, to reduce lag. Issues come from CTouchControls.
1404void CMenusIngameTouchControls::ResolveIssues()
1405{
1406 if(GameClient()->m_TouchControls.AnyIssueNotResolved())
1407 {
1408 std::array<CTouchControls::CIssueParam, (unsigned)CTouchControls::EIssueType::NUM_ISSUES> aIssues = GameClient()->m_TouchControls.Issues();
1409 for(unsigned Current = 0; Current < (unsigned)CTouchControls::EIssueType::NUM_ISSUES; Current++)
1410 {
1411 if(aIssues[Current].m_Resolved == true)
1412 continue;
1413 switch(Current)
1414 {
1415 case(int)CTouchControls::EIssueType::CACHE_SETTINGS: CacheAllSettingsFromTarget(pTargetButton: aIssues[Current].m_pTargetButton); break;
1416 case(int)CTouchControls::EIssueType::SAVE_SETTINGS:
1417 {
1418 if(CheckCachedSettings())
1419 SaveCachedSettingsToTarget(pTargetButton: aIssues[Current].m_pTargetButton);
1420 break;
1421 }
1422 case(int)CTouchControls::EIssueType::CACHE_POSITION: SetPosInputs(aIssues[Current].m_pTargetButton->m_UnitRect); break;
1423 default: dbg_assert_failed("Unknown Issue type: %d", Current);
1424 }
1425 }
1426 }
1427}
1428
1429// Turn predefined behavior strings like "joystick-hook" into integers according to the enum.
1430int CMenusIngameTouchControls::CalculatePredefinedType(const char *pType) const
1431{
1432 int IntegerType = (int)EPredefinedType::INGAME_MENU;
1433 static_assert((int)EPredefinedType::INGAME_MENU == 0, "This should start from the first predefined type");
1434 for(; IntegerType < (int)EPredefinedType::NUM_PREDEFINEDTYPES; IntegerType++)
1435 {
1436 if(str_comp(a: pType, b: BEHAVIOR_FACTORIES_EDITOR[IntegerType].m_pId) == 0)
1437 return IntegerType;
1438 }
1439 dbg_assert_failed("Unknown predefined type %s.", pType == nullptr ? "nullptr" : pType);
1440}
1441
1442std::string CMenusIngameTouchControls::DetermineTouchButtonCommandLabel(CTouchControls::CTouchButton *pButton) const
1443{
1444 const char *pBehaviorType = pButton->m_pBehavior->GetBehaviorType();
1445 if(str_comp(a: pBehaviorType, b: CTouchControls::CBindTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1446 {
1447 return static_cast<CTouchControls::CBindTouchButtonBehavior *>(pButton->m_pBehavior.get())->GetCommand();
1448 }
1449 else if(str_comp(a: pBehaviorType, b: CTouchControls::CBindToggleTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1450 {
1451 const auto *pBehavior = static_cast<CTouchControls::CBindToggleTouchButtonBehavior *>(pButton->m_pBehavior.get());
1452 return pBehavior->GetCommand()[pBehavior->GetActiveCommandIndex()].m_Command;
1453 }
1454 else if(str_comp(a: pBehaviorType, b: CTouchControls::CPredefinedTouchButtonBehavior::BEHAVIOR_TYPE) == 0)
1455 {
1456 auto *pTargetBehavior = static_cast<CTouchControls::CPredefinedTouchButtonBehavior *>(pButton->m_pBehavior.get());
1457 const char *pPredefinedType = pTargetBehavior->GetPredefinedType();
1458 const char **apPredefinedNames = PredefinedNames();
1459 std::string Command = apPredefinedNames[CalculatePredefinedType(pType: pPredefinedType)];
1460 if(str_comp(a: pPredefinedType, b: CTouchControls::CExtraMenuTouchButtonBehavior::BEHAVIOR_ID) == 0)
1461 {
1462 const auto *pExtraMenuBehavior = static_cast<CTouchControls::CExtraMenuTouchButtonBehavior *>(pTargetBehavior);
1463 Command.append(s: " ");
1464 Command.append(str: std::to_string(val: pExtraMenuBehavior->GetNumber() + 1));
1465 }
1466 return Command;
1467 }
1468 else
1469 {
1470 dbg_assert_failed("Detected unknown behavior type in CMenusIngameTouchControls::GetCommand()");
1471 }
1472}
1473
1474// Used for making json chars like \n or \uf3ce visible.
1475std::string CMenusIngameTouchControls::CBehaviorElements::ParseLabel(const char *pLabel) const
1476{
1477 json_settings JsonSettings{};
1478 char aError[256];
1479 char aJsonString[1048];
1480 str_format(buffer: aJsonString, buffer_size: sizeof(aJsonString), format: "\"%s\"", pLabel);
1481 json_value *pJsonLabel = JsonParseEx(pSettings: &JsonSettings, pJson: aJsonString, Length: str_length(str: aJsonString), pError: aError);
1482 if(pJsonLabel == nullptr)
1483 {
1484 return pLabel;
1485 }
1486 dbg_assert(pJsonLabel->type == json_string, "Parsed label must be string");
1487 std::string ParsedString = pJsonLabel->u.string.ptr;
1488 json_value_free(pJsonLabel);
1489 return ParsedString;
1490}
1491
1492CMenusIngameTouchControls::CBehaviorElements::CBehaviorElements() noexcept
1493{
1494 Reset();
1495}
1496
1497CMenusIngameTouchControls::CBehaviorElements::CBehaviorElements(CBehaviorElements &&Other) noexcept :
1498 m_InputCommand(std::move(Other.m_InputCommand)), m_InputLabel(std::move(Other.m_InputLabel)), m_CachedCommands(std::move(Other.m_CachedCommands))
1499{
1500}
1501
1502CMenusIngameTouchControls::CBehaviorElements::~CBehaviorElements()
1503{
1504 m_InputCommand.Deactivate();
1505 m_InputLabel.Deactivate();
1506}
1507
1508CMenusIngameTouchControls::CBehaviorElements &CMenusIngameTouchControls::CBehaviorElements::operator=(CBehaviorElements &&Other) noexcept
1509{
1510 if(this == &Other)
1511 {
1512 return *this;
1513 }
1514 m_InputCommand = std::move(Other.m_InputCommand);
1515 m_InputLabel = std::move(Other.m_InputLabel);
1516 m_CachedCommands = std::move(Other.m_CachedCommands);
1517 return *this;
1518}
1519
1520void CMenusIngameTouchControls::CBehaviorElements::UpdateInputs()
1521{
1522 m_InputLabel.Set(ParseLabel(pLabel: m_CachedCommands.m_Label.c_str()).c_str());
1523 m_InputCommand.Set(m_CachedCommands.m_Command.c_str());
1524}
1525
1526void CMenusIngameTouchControls::CBehaviorElements::Reset()
1527{
1528 m_CachedCommands = {};
1529 m_InputCommand.Clear();
1530 m_InputLabel.Clear();
1531}
1532
1533const char **CMenusIngameTouchControls::VisibilityNames() const
1534{
1535 static const char *s_apVisibilities[8];
1536 s_apVisibilities[0] = Localize(pStr: "Ingame", pContext: "Touch button visibilities");
1537 s_apVisibilities[1] = Localize(pStr: "Zoom Allowed", pContext: "Touch button visibilities");
1538 s_apVisibilities[2] = Localize(pStr: "Vote Active", pContext: "Touch button visibilities");
1539 s_apVisibilities[3] = Localize(pStr: "Dummy Allowed", pContext: "Touch button visibilities");
1540 s_apVisibilities[4] = Localize(pStr: "Dummy Connected", pContext: "Touch button visibilities");
1541 s_apVisibilities[5] = Localize(pStr: "Rcon Authed", pContext: "Touch button visibilities");
1542 s_apVisibilities[6] = Localize(pStr: "Demo Player", pContext: "Touch button visibilities");
1543 s_apVisibilities[7] = Localize(pStr: "Extra Menu", pContext: "Touch button visibilities");
1544 static_assert(std::size(s_apVisibilities) == (size_t)CTouchControls::EButtonVisibility::NUM_VISIBILITIES - CTouchControls::MAX_EXTRA_MENU_NUMBER + 1, "Insufficient visibility names");
1545 return s_apVisibilities;
1546}
1547
1548const char **CMenusIngameTouchControls::PredefinedNames() const
1549{
1550 static const char *s_apPredefined[11];
1551 s_apPredefined[0] = Localize(pStr: "Ingame Menu", pContext: "Predefined touch button behaviors");
1552 s_apPredefined[1] = Localize(pStr: "Extra Menu", pContext: "Predefined touch button behaviors");
1553 s_apPredefined[2] = Localize(pStr: "Emoticon", pContext: "Predefined touch button behaviors");
1554 s_apPredefined[3] = Localize(pStr: "Spectate", pContext: "Predefined touch button behaviors");
1555 s_apPredefined[4] = Localize(pStr: "Swap Action", pContext: "Predefined touch button behaviors");
1556 s_apPredefined[5] = Localize(pStr: "Use Action", pContext: "Predefined touch button behaviors");
1557 s_apPredefined[6] = Localize(pStr: "Joystick Action", pContext: "Predefined touch button behaviors");
1558 s_apPredefined[7] = Localize(pStr: "Joystick Aim", pContext: "Predefined touch button behaviors");
1559 s_apPredefined[8] = Localize(pStr: "Joystick Relative Aim", pContext: "Predefined touch button behaviors");
1560 s_apPredefined[9] = Localize(pStr: "Joystick Fire", pContext: "Predefined touch button behaviors");
1561 s_apPredefined[10] = Localize(pStr: "Joystick Hook", pContext: "Predefined touch button behaviors");
1562 static_assert(std::size(s_apPredefined) == std::size(BEHAVIOR_FACTORIES_EDITOR), "Insufficient predefined names");
1563 return s_apPredefined;
1564}
1565
1566const char *CMenusIngameTouchControls::HelpMessageForBehaviorType(EBehaviorType Type) const
1567{
1568 switch(m_EditBehaviorType)
1569 {
1570 case EBehaviorType::BIND: return Localize(pStr: "Buttons with this behavior execute console commands like regular key binds."); break;
1571 case EBehaviorType::BIND_TOGGLE: return Localize(pStr: "Buttons with this behavior cycle between executing one of two or more specified commands."); break;
1572 case EBehaviorType::PREDEFINED: return Localize(pStr: "Buttons with this behavior execute some predefined actions."); break;
1573 default: dbg_assert_failed("Unknown behavior %d", (int)m_EditBehaviorType);
1574 }
1575 static_assert((int)EBehaviorType::NUM_BEHAVIORS == 3, "Insufficient help messages");
1576}
1577
1578const char *CMenusIngameTouchControls::HelpMessageForPredefinedType(EPredefinedType Type) const
1579{
1580 switch(m_PredefinedBehaviorType)
1581 {
1582 case EPredefinedType::INGAME_MENU: return Localize(pStr: "Opens the ingame menu immediately when released."); break;
1583 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;
1584 case EPredefinedType::EMOTICON: return Localize(pStr: "Opens the emoticon selector (this does not work with binds)."); break;
1585 case EPredefinedType::SPECTATE: return Localize(pStr: "Opens the spectator menu (this does not work with binds)."); break;
1586 case EPredefinedType::SWAP_ACTION: return Localize(pStr: "Swaps the active action (fire and hook) for direct touch input and virtual joysticks."); break;
1587 case EPredefinedType::USE_ACTION: return Localize(pStr: "Uses the active action with the current aiming position."); break;
1588 case EPredefinedType::JOYSTICK_ACTION: return Localize(pStr: "Virtual joystick which uses the active action."); break;
1589 case EPredefinedType::JOYSTICK_AIM: return Localize(pStr: "Virtual joystick which only aims without using an action."); break;
1590 case EPredefinedType::JOYSTICK_AIM_RELATIVE: return Localize(pStr: "Virtual joystick which only aims and moves the mouse pointer relatively."); break;
1591 case EPredefinedType::JOYSTICK_FIRE: return Localize(pStr: "Virtual joystick which always uses fire."); break;
1592 case EPredefinedType::JOYSTICK_HOOK: return Localize(pStr: "Virtual joystick which always uses hook."); break;
1593 default: dbg_assert_failed("Unknown behavior %d", (int)m_PredefinedBehaviorType);
1594 }
1595 static_assert((int)EPredefinedType::NUM_PREDEFINEDTYPES == 11, "Insufficient help messages");
1596}
1597
1598const char *CMenusIngameTouchControls::HelpMessageForVisibilityType(CTouchControls::EButtonVisibility Type) const
1599{
1600 switch(Type)
1601 {
1602 case CTouchControls::EButtonVisibility::INGAME: return Localize(pStr: "Player is ingame, i.e. not spectating."); break;
1603 case CTouchControls::EButtonVisibility::ZOOM_ALLOWED: return Localize(pStr: "Zoom is allowed on this server."); break;
1604 case CTouchControls::EButtonVisibility::VOTE_ACTIVE: return Localize(pStr: "A vote is currently active."); break;
1605 case CTouchControls::EButtonVisibility::DUMMY_ALLOWED: return Localize(pStr: "Dummy is allowed on this server."); break;
1606 case CTouchControls::EButtonVisibility::DUMMY_CONNECTED: return Localize(pStr: "Dummy is currently connected."); break;
1607 case CTouchControls::EButtonVisibility::RCON_AUTHED: return Localize(pStr: "Player is currently authed in rcon."); break;
1608 case CTouchControls::EButtonVisibility::DEMO_PLAYER: return Localize(pStr: "Demo player is currently active."); break;
1609 case CTouchControls::EButtonVisibility::EXTRA_MENU_1: return Localize(pStr: "The extra menu with the given number is activated."); break;
1610 default: dbg_assert_failed("Unknown visibility type %d", (int)Type);
1611 }
1612 static_assert((int)CTouchControls::EButtonVisibility::NUM_VISIBILITIES - CTouchControls::MAX_EXTRA_MENU_NUMBER + 1 == 8, "Insufficient help messages");
1613}
1614