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