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