1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include <base/hash.h>
5#include <base/math.h>
6#include <base/system.h>
7
8#include <engine/demo.h>
9#include <engine/graphics.h>
10#include <engine/keys.h>
11#include <engine/shared/localization.h>
12#include <engine/storage.h>
13#include <engine/textrender.h>
14
15#include <game/client/components/console.h>
16#include <game/client/gameclient.h>
17#include <game/client/render.h>
18#include <game/client/ui.h>
19#include <game/client/ui_listbox.h>
20#include <game/generated/client_data.h>
21#include <game/localization.h>
22
23#include "maplayers.h"
24#include "menus.h"
25
26#include <chrono>
27
28using namespace FontIcons;
29using namespace std::chrono_literals;
30
31int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, bool Enabled)
32{
33 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * Ui()->ButtonColorMul(pId: pButtonContainer)), Corners, Rounding: 5.0f);
34
35 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
36 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
37 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
38 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
39 CUIRect Temp;
40 pRect->HMargin(Cut: 2.0f, pOtherRect: &Temp);
41 Ui()->DoLabel(pRect: &Temp, pText, Size: Temp.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
42
43 if(!Enabled)
44 {
45 TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
46 TextRender()->TextOutlineColor(rgb: ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f));
47 Ui()->DoLabel(pRect: &Temp, pText: FONT_ICON_SLASH, Size: Temp.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
48 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
49 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
50 }
51
52 TextRender()->SetRenderFlags(0);
53 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
54
55 return Ui()->DoButtonLogic(pId: pButtonContainer, Checked, pRect);
56}
57
58bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser)
59{
60 bool DoFilterChat = *(bool *)pUser;
61 if(!DoFilterChat)
62 {
63 return false;
64 }
65
66 CUnpacker Unpacker;
67 Unpacker.Reset(pData, Size);
68
69 int Msg = Unpacker.GetInt();
70 int Sys = Msg & 1;
71 Msg >>= 1;
72
73 return !Unpacker.Error() && !Sys && Msg == NETMSGTYPE_SV_CHAT;
74}
75
76void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek)
77{
78 if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f)
79 {
80 m_pClient->m_Chat.Reset();
81 m_pClient->m_InfoMessages.OnReset();
82 m_pClient->m_Particles.OnReset();
83 m_pClient->m_Sounds.OnReset();
84 m_pClient->m_Scoreboard.OnReset();
85 m_pClient->m_Statboard.OnReset();
86 m_pClient->m_SuppressEvents = true;
87 if(TimeToSeek != 0.0f)
88 DemoPlayer()->SeekTime(Seconds: TimeToSeek);
89 else
90 DemoPlayer()->SeekPercent(Percent: PositionToSeek);
91 m_pClient->m_SuppressEvents = false;
92 m_pClient->m_MapLayersBackground.EnvelopeUpdate();
93 m_pClient->m_MapLayersForeground.EnvelopeUpdate();
94 if(!DemoPlayer()->BaseInfo()->m_Paused && PositionToSeek == 1.0f)
95 DemoPlayer()->Pause();
96 }
97}
98
99void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset)
100{
101 m_pClient->m_SuppressEvents = true;
102 DemoPlayer()->SeekTick(TickOffset);
103 m_pClient->m_SuppressEvents = false;
104 DemoPlayer()->Pause();
105 m_pClient->m_MapLayersBackground.EnvelopeUpdate();
106 m_pClient->m_MapLayersForeground.EnvelopeUpdate();
107}
108
109void CMenus::RenderDemoPlayer(CUIRect MainView)
110{
111 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
112 const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick;
113 const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick;
114
115 // When rendering a demo and starting paused, render the pause indicator permanently.
116#if defined(CONF_VIDEORECORDER)
117 const bool VideoRendering = IVideo::Current() != nullptr;
118 bool InitialVideoPause = VideoRendering && m_LastPauseChange < 0.0f && pInfo->m_Paused;
119#else
120 const bool VideoRendering = false;
121 bool InitialVideoPause = false;
122#endif
123
124 const auto &&UpdateLastPauseChange = [&]() {
125 // Immediately hide the pause indicator when unpausing the initial pause when rendering a demo.
126 m_LastPauseChange = InitialVideoPause ? 0.0f : Client()->GlobalTime();
127 InitialVideoPause = false;
128 };
129 const auto &&UpdateLastSpeedChange = [&]() {
130 m_LastSpeedChange = Client()->GlobalTime();
131 };
132
133 // threshold value, accounts for slight inaccuracy when setting demo position
134 constexpr int Threshold = 10;
135 const auto &&FindPreviousMarkerPosition = [&]() {
136 for(int i = pInfo->m_NumTimelineMarkers - 1; i >= 0; i--)
137 {
138 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) < CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
139 {
140 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
141 }
142 }
143 return 0.0f;
144 };
145 const auto &&FindNextMarkerPosition = [&]() {
146 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
147 {
148 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) > CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
149 {
150 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
151 }
152 }
153 return 1.0f;
154 };
155
156 static int s_SkipDurationIndex = 1;
157 static const int s_aSkipDurationsSeconds[] = {1, 5, 10, 30, 60, 5 * 60, 10 * 60};
158 const int DemoLengthSeconds = TotalTicks / Client()->GameTickSpeed();
159 int NumDurationLabels = 0;
160 for(size_t i = 0; i < std::size(s_aSkipDurationsSeconds); ++i)
161 {
162 if(s_aSkipDurationsSeconds[i] >= DemoLengthSeconds)
163 break;
164 NumDurationLabels = i + 1;
165 }
166 if(NumDurationLabels > 0 && s_SkipDurationIndex >= NumDurationLabels)
167 s_SkipDurationIndex = maximum(a: 0, b: NumDurationLabels - 1);
168
169 // handle keyboard shortcuts independent of active menu
170 float PositionToSeek = -1.0f;
171 float TimeToSeek = 0.0f;
172 if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !Ui()->IsPopupOpen())
173 {
174 // increase/decrease speed
175 if(!Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed())
176 {
177 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP) || Input()->KeyPress(Key: KEY_UP))
178 {
179 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
180 UpdateLastSpeedChange();
181 }
182 else if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN) || Input()->KeyPress(Key: KEY_DOWN))
183 {
184 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
185 UpdateLastSpeedChange();
186 }
187 }
188
189 // pause/unpause
190 if(Input()->KeyPress(Key: KEY_SPACE) || Input()->KeyPress(Key: KEY_RETURN) || Input()->KeyPress(Key: KEY_KP_ENTER) || Input()->KeyPress(Key: KEY_K))
191 {
192 if(pInfo->m_Paused)
193 {
194 DemoPlayer()->Unpause();
195 }
196 else
197 {
198 DemoPlayer()->Pause();
199 }
200 UpdateLastPauseChange();
201 }
202
203 // seek backward/forward configured time
204 if(Input()->KeyPress(Key: KEY_LEFT) || Input()->KeyPress(Key: KEY_J))
205 {
206 if(Input()->ModifierIsPressed())
207 PositionToSeek = FindPreviousMarkerPosition();
208 else if(Input()->ShiftIsPressed())
209 s_SkipDurationIndex = maximum(a: s_SkipDurationIndex - 1, b: 0);
210 else
211 TimeToSeek = -s_aSkipDurationsSeconds[s_SkipDurationIndex];
212 }
213 else if(Input()->KeyPress(Key: KEY_RIGHT) || Input()->KeyPress(Key: KEY_L))
214 {
215 if(Input()->ModifierIsPressed())
216 PositionToSeek = FindNextMarkerPosition();
217 else if(Input()->ShiftIsPressed())
218 s_SkipDurationIndex = minimum(a: s_SkipDurationIndex + 1, b: NumDurationLabels - 1);
219 else
220 TimeToSeek = s_aSkipDurationsSeconds[s_SkipDurationIndex];
221 }
222
223 // seek to 0-90%
224 const int aSeekPercentKeys[] = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9};
225 for(unsigned i = 0; i < std::size(aSeekPercentKeys); i++)
226 {
227 if(Input()->KeyPress(Key: aSeekPercentKeys[i]))
228 {
229 PositionToSeek = i * 0.1f;
230 break;
231 }
232 }
233
234 // seek to the beginning/end
235 if(Input()->KeyPress(Key: KEY_HOME))
236 {
237 PositionToSeek = 0.0f;
238 }
239 else if(Input()->KeyPress(Key: KEY_END))
240 {
241 PositionToSeek = 1.0f;
242 }
243
244 // Advance single frame forward/backward with period/comma key
245 if(Input()->KeyPress(Key: KEY_PERIOD))
246 {
247 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
248 }
249 else if(Input()->KeyPress(Key: KEY_COMMA))
250 {
251 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
252 }
253 }
254
255 const float SeekBarHeight = 15.0f;
256 const float ButtonbarHeight = 20.0f;
257 const float NameBarHeight = 20.0f;
258 const float Margins = 5.0f;
259 const float TotalHeight = SeekBarHeight + ButtonbarHeight + NameBarHeight + Margins * 3;
260
261 if(!m_MenuActive)
262 {
263 // Render pause indicator
264 if(g_Config.m_ClDemoShowPause && (InitialVideoPause || (!VideoRendering && Client()->GlobalTime() - m_LastPauseChange < 0.5f)))
265 {
266 const float Time = InitialVideoPause ? 0.5f : ((Client()->GlobalTime() - m_LastPauseChange) / 0.5f);
267 const float Alpha = (Time < 0.5f ? Time : (1.0f - Time)) * 2.0f;
268 if(Alpha > 0.0f)
269 {
270 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor().WithMultipliedAlpha(alpha: Alpha));
271 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(alpha: Alpha));
272 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
273 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
274 Ui()->DoLabel(pRect: Ui()->Screen(), pText: pInfo->m_Paused ? FONT_ICON_PAUSE : FONT_ICON_PLAY, Size: 36.0f + Time * 12.0f, Align: TEXTALIGN_MC);
275 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
276 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
277 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
278 TextRender()->SetRenderFlags(0);
279 }
280 }
281
282 // Render speed info
283 if(g_Config.m_ClDemoShowSpeed && Client()->GlobalTime() - m_LastSpeedChange < 1.0f)
284 {
285 CUIRect Screen = *Ui()->Screen();
286
287 char aSpeedBuf[16];
288 str_format(buffer: aSpeedBuf, buffer_size: sizeof(aSpeedBuf), format: "×%.2f", pInfo->m_Speed);
289 TextRender()->Text(x: 120.0f, y: Screen.y + Screen.h - 120.0f - TotalHeight, Size: 60.0f, pText: aSpeedBuf, LineWidth: -1.0f);
290 }
291 }
292 else
293 {
294 if(m_LastPauseChange > 0.0f)
295 m_LastPauseChange = 0.0f;
296 if(m_LastSpeedChange > 0.0f)
297 m_LastSpeedChange = 0.0f;
298 }
299
300 if(CurrentTick == TotalTicks)
301 {
302 DemoPlayer()->Pause();
303 PositionToSeek = 0.0f;
304 UpdateLastPauseChange();
305 }
306
307 if(!m_MenuActive)
308 {
309 HandleDemoSeeking(PositionToSeek, TimeToSeek);
310 return;
311 }
312
313 CUIRect DemoControls;
314 MainView.HSplitBottom(Cut: TotalHeight, pTop: nullptr, pBottom: &DemoControls);
315 DemoControls.VSplitLeft(Cut: 50.0f, pLeft: nullptr, pRight: &DemoControls);
316 DemoControls.VSplitLeft(Cut: 600.0f, pLeft: &DemoControls, pRight: nullptr);
317 const CUIRect DemoControlsOriginal = DemoControls;
318 DemoControls.x += m_DemoControlsPositionOffset.x;
319 DemoControls.y += m_DemoControlsPositionOffset.y;
320 int Corners = IGraphics::CORNER_NONE;
321 if(DemoControls.x > 0.0f && DemoControls.y > 0.0f)
322 Corners |= IGraphics::CORNER_TL;
323 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y > 0.0f)
324 Corners |= IGraphics::CORNER_TR;
325 if(DemoControls.x > 0.0f && DemoControls.y < MainView.h - DemoControls.h)
326 Corners |= IGraphics::CORNER_BL;
327 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y < MainView.h - DemoControls.h)
328 Corners |= IGraphics::CORNER_BR;
329 DemoControls.Draw(Color: ms_ColorTabbarActive, Corners, Rounding: 10.0f);
330 const CUIRect DemoControlsDragRect = DemoControls;
331
332 CUIRect SeekBar, ButtonBar, NameBar, SpeedBar;
333 DemoControls.Margin(Cut: 5.0f, pOtherRect: &DemoControls);
334 DemoControls.HSplitTop(Cut: SeekBarHeight, pTop: &SeekBar, pBottom: &ButtonBar);
335 ButtonBar.HSplitTop(Cut: Margins, pTop: nullptr, pBottom: &ButtonBar);
336 ButtonBar.HSplitBottom(Cut: NameBarHeight, pTop: &ButtonBar, pBottom: &NameBar);
337 NameBar.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &NameBar);
338
339 // handle draggable demo controls
340 {
341 enum EDragOperation
342 {
343 OP_NONE,
344 OP_DRAGGING,
345 OP_CLICKED
346 };
347 static EDragOperation s_Operation = OP_NONE;
348 static vec2 s_InitialMouse = vec2(0.0f, 0.0f);
349
350 bool Clicked;
351 bool Abrupted;
352 if(int Result = Ui()->DoDraggableButtonLogic(pId: &s_Operation, Checked: 8, pRect: &DemoControlsDragRect, pClicked: &Clicked, pAbrupted: &Abrupted))
353 {
354 if(s_Operation == OP_NONE && Result == 1)
355 {
356 s_InitialMouse = Ui()->MousePos();
357 s_Operation = OP_CLICKED;
358 }
359
360 if(Clicked || Abrupted)
361 s_Operation = OP_NONE;
362
363 if(s_Operation == OP_CLICKED && length(a: Ui()->MousePos() - s_InitialMouse) > 5.0f)
364 {
365 s_Operation = OP_DRAGGING;
366 s_InitialMouse -= m_DemoControlsPositionOffset;
367 }
368
369 if(s_Operation == OP_DRAGGING)
370 {
371 m_DemoControlsPositionOffset = Ui()->MousePos() - s_InitialMouse;
372 m_DemoControlsPositionOffset.x = clamp(val: m_DemoControlsPositionOffset.x, lo: -DemoControlsOriginal.x, hi: MainView.w - DemoControlsDragRect.w - DemoControlsOriginal.x);
373 m_DemoControlsPositionOffset.y = clamp(val: m_DemoControlsPositionOffset.y, lo: -DemoControlsOriginal.y, hi: MainView.h - DemoControlsDragRect.h - DemoControlsOriginal.y);
374 }
375 }
376 }
377
378 // do seekbar
379 {
380 const float Rounding = 5.0f;
381
382 static int s_SeekBarId = 0;
383 void *pId = &s_SeekBarId;
384 char aBuffer[128];
385
386 // draw seek bar
387 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
388
389 // draw filled bar
390 float Amount = CurrentTick / (float)TotalTicks;
391 CUIRect FilledBar = SeekBar;
392 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
393 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
394
395 // draw highlighting
396 if(g_Config.m_ClDemoSliceBegin != -1 && g_Config.m_ClDemoSliceEnd != -1)
397 {
398 float RatioBegin = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
399 float RatioEnd = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
400 float Span = ((SeekBar.w - 2 * Rounding) * RatioEnd) - ((SeekBar.w - 2 * Rounding) * RatioBegin);
401 Graphics()->TextureClear();
402 Graphics()->QuadsBegin();
403 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 0.25f);
404 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * RatioBegin, SeekBar.y, Span, SeekBar.h);
405 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
406 Graphics()->QuadsEnd();
407 }
408
409 // draw markers
410 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
411 {
412 float Ratio = (pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / (float)TotalTicks;
413 Graphics()->TextureClear();
414 Graphics()->QuadsBegin();
415 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
416 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
417 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
418 Graphics()->QuadsEnd();
419 }
420
421 // draw slice markers
422 // begin
423 if(g_Config.m_ClDemoSliceBegin != -1)
424 {
425 float Ratio = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
426 Graphics()->TextureClear();
427 Graphics()->QuadsBegin();
428 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
429 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
430 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
431 Graphics()->QuadsEnd();
432 }
433
434 // end
435 if(g_Config.m_ClDemoSliceEnd != -1)
436 {
437 float Ratio = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
438 Graphics()->TextureClear();
439 Graphics()->QuadsBegin();
440 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
441 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
442 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
443 Graphics()->QuadsEnd();
444 }
445
446 // draw time
447 char aCurrentTime[32];
448 str_time(centisecs: (int64_t)CurrentTick / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
449 char aTotalTime[32];
450 str_time(centisecs: (int64_t)TotalTicks / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
451 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s / %s", aCurrentTime, aTotalTime);
452 Ui()->DoLabel(pRect: &SeekBar, pText: aBuffer, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
453
454 // do the logic
455 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
456
457 if(Ui()->CheckActiveItem(pId))
458 {
459 if(!Ui()->MouseButton(Index: 0))
460 Ui()->SetActiveItem(nullptr);
461 else
462 {
463 static float s_PrevAmount = 0.0f;
464 float AmountSeek = clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
465
466 if(Input()->ShiftIsPressed())
467 {
468 AmountSeek = s_PrevAmount + (AmountSeek - s_PrevAmount) * 0.05f;
469 if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(a: s_PrevAmount - AmountSeek) >= 0.0001f)
470 {
471 PositionToSeek = AmountSeek;
472 }
473 }
474 else
475 {
476 if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(a: s_PrevAmount - AmountSeek) >= 0.001f)
477 {
478 s_PrevAmount = AmountSeek;
479 PositionToSeek = AmountSeek;
480 }
481 }
482 }
483 }
484 else if(Ui()->HotItem() == pId)
485 {
486 if(Ui()->MouseButton(Index: 0))
487 {
488 Ui()->SetActiveItem(pId);
489 }
490 }
491
492 if(Inside && !Ui()->MouseButton(Index: 0))
493 Ui()->SetHotItem(pId);
494
495 if(Ui()->HotItem() == pId)
496 {
497 const int HoveredTick = (int)(clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f) * TotalTicks);
498 static char s_aHoveredTime[32];
499 str_time(centisecs: (int64_t)HoveredTick / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: s_aHoveredTime, buffer_size: sizeof(s_aHoveredTime));
500 GameClient()->m_Tooltips.DoToolTip(pId, pNearRect: &SeekBar, pText: s_aHoveredTime);
501 }
502 }
503
504 bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false;
505
506 // do buttons
507 CUIRect Button;
508
509 // combined play and pause button
510 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
511 static CButtonContainer s_PlayPauseButton;
512 if(DoButton_FontIcon(pButtonContainer: &s_PlayPauseButton, pText: pInfo->m_Paused ? FONT_ICON_PLAY : FONT_ICON_PAUSE, Checked: false, pRect: &Button, Corners: IGraphics::CORNER_ALL))
513 {
514 if(pInfo->m_Paused)
515 {
516 DemoPlayer()->Unpause();
517 }
518 else
519 {
520 DemoPlayer()->Pause();
521 }
522 UpdateLastPauseChange();
523 }
524 GameClient()->m_Tooltips.DoToolTip(pId: &s_PlayPauseButton, pNearRect: &Button, pText: pInfo->m_Paused ? Localize(pStr: "Play the current demo") : Localize(pStr: "Pause the current demo"));
525
526 // stop button
527 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
528 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
529 static CButtonContainer s_ResetButton;
530 if(DoButton_FontIcon(pButtonContainer: &s_ResetButton, pText: FONT_ICON_STOP, Checked: false, pRect: &Button, Corners: IGraphics::CORNER_ALL))
531 {
532 DemoPlayer()->Pause();
533 PositionToSeek = 0.0f;
534 }
535 GameClient()->m_Tooltips.DoToolTip(pId: &s_ResetButton, pNearRect: &Button, pText: Localize(pStr: "Stop the current demo"));
536
537 // skip time back
538 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
539 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
540 static CButtonContainer s_TimeBackButton;
541 if(DoButton_FontIcon(pButtonContainer: &s_TimeBackButton, pText: FONT_ICON_BACKWARD, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
542 {
543 TimeToSeek = -s_aSkipDurationsSeconds[s_SkipDurationIndex];
544 }
545 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back the specified duration"));
546
547 // skip time dropdown
548 if(NumDurationLabels >= 2)
549 {
550 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
551 ButtonBar.VSplitLeft(Cut: 4 * ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
552
553 static std::vector<std::string> s_vDurationNames;
554 static std::vector<const char *> s_vpDurationNames;
555 s_vDurationNames.resize(new_size: NumDurationLabels);
556 s_vpDurationNames.resize(new_size: NumDurationLabels);
557
558 for(int i = 0; i < NumDurationLabels; ++i)
559 {
560 char aBuf[256];
561 if(s_aSkipDurationsSeconds[i] >= 60)
562 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d min.", pContext: "Demo player duration"), s_aSkipDurationsSeconds[i] / 60);
563 else
564 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d sec.", pContext: "Demo player duration"), s_aSkipDurationsSeconds[i]);
565 s_vDurationNames[i] = aBuf;
566 s_vpDurationNames[i] = s_vDurationNames[i].c_str();
567 }
568
569 static CUi::SDropDownState s_SkipDurationDropDownState;
570 static CScrollRegion s_SkipDurationDropDownScrollRegion;
571 s_SkipDurationDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_SkipDurationDropDownScrollRegion;
572 s_SkipDurationIndex = Ui()->DoDropDown(pRect: &Button, CurSelection: s_SkipDurationIndex, pStrs: s_vpDurationNames.data(), Num: NumDurationLabels, State&: s_SkipDurationDropDownState);
573 GameClient()->m_Tooltips.DoToolTip(pId: &s_SkipDurationDropDownState.m_ButtonContainer, pNearRect: &Button, pText: Localize(pStr: "Change the skip duration"));
574 }
575
576 // skip time forward
577 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
578 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
579 static CButtonContainer s_TimeForwardButton;
580 if(DoButton_FontIcon(pButtonContainer: &s_TimeForwardButton, pText: FONT_ICON_FORWARD, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
581 {
582 TimeToSeek = s_aSkipDurationsSeconds[s_SkipDurationIndex];
583 }
584 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward the specified duration"));
585
586 // one tick back
587 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
588 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
589 static CButtonContainer s_OneTickBackButton;
590 if(DoButton_FontIcon(pButtonContainer: &s_OneTickBackButton, pText: FONT_ICON_BACKWARD_STEP, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
591 {
592 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
593 }
594 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one tick"));
595
596 // one tick forward
597 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
598 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
599 static CButtonContainer s_OneTickForwardButton;
600 if(DoButton_FontIcon(pButtonContainer: &s_OneTickForwardButton, pText: FONT_ICON_FORWARD_STEP, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
601 {
602 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
603 }
604 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one tick"));
605
606 // one marker back
607 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
608 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
609 static CButtonContainer s_OneMarkerBackButton;
610 if(DoButton_FontIcon(pButtonContainer: &s_OneMarkerBackButton, pText: FONT_ICON_BACKWARD_FAST, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
611 {
612 PositionToSeek = FindPreviousMarkerPosition();
613 }
614 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one marker"));
615
616 // one marker forward
617 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
618 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
619 static CButtonContainer s_OneMarkerForwardButton;
620 if(DoButton_FontIcon(pButtonContainer: &s_OneMarkerForwardButton, pText: FONT_ICON_FORWARD_FAST, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
621 {
622 PositionToSeek = FindNextMarkerPosition();
623 }
624 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one marker"));
625
626 // slowdown
627 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: 0, pRight: &ButtonBar);
628 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
629 static CButtonContainer s_SlowDownButton;
630 if(DoButton_FontIcon(pButtonContainer: &s_SlowDownButton, pText: FONT_ICON_CHEVRON_DOWN, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
631 DecreaseDemoSpeed = true;
632 GameClient()->m_Tooltips.DoToolTip(pId: &s_SlowDownButton, pNearRect: &Button, pText: Localize(pStr: "Slow down the demo"));
633
634 // fastforward
635 ButtonBar.VSplitLeft(Cut: Margins, pLeft: 0, pRight: &ButtonBar);
636 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
637 static CButtonContainer s_SpeedUpButton;
638 if(DoButton_FontIcon(pButtonContainer: &s_SpeedUpButton, pText: FONT_ICON_CHEVRON_UP, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL))
639 IncreaseDemoSpeed = true;
640 GameClient()->m_Tooltips.DoToolTip(pId: &s_SpeedUpButton, pNearRect: &Button, pText: Localize(pStr: "Speed up the demo"));
641
642 // speed meter
643 ButtonBar.VSplitLeft(Cut: Margins * 12, pLeft: &SpeedBar, pRight: &ButtonBar);
644 char aBuffer[64];
645 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "×%g", pInfo->m_Speed);
646 Ui()->DoLabel(pRect: &SpeedBar, pText: aBuffer, Size: Button.h * 0.7f, Align: TEXTALIGN_MC);
647
648 // slice begin button
649 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
650 static CButtonContainer s_SliceBeginButton;
651 const int SliceBeginButtonResult = DoButton_FontIcon(pButtonContainer: &s_SliceBeginButton, pText: FONT_ICON_RIGHT_FROM_BRACKET, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL);
652 if(SliceBeginButtonResult == 1)
653 {
654 Client()->DemoSliceBegin();
655 if(CurrentTick > (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick))
656 g_Config.m_ClDemoSliceEnd = -1;
657 }
658 else if(SliceBeginButtonResult == 2)
659 {
660 g_Config.m_ClDemoSliceBegin = -1;
661 }
662 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceBeginButton, pNearRect: &Button, pText: Localize(pStr: "Mark the beginning of a cut (right click to reset)"));
663
664 // slice end button
665 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
666 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
667 static CButtonContainer s_SliceEndButton;
668 const int SliceEndButtonResult = DoButton_FontIcon(pButtonContainer: &s_SliceEndButton, pText: FONT_ICON_RIGHT_TO_BRACKET, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL);
669 if(SliceEndButtonResult == 1)
670 {
671 Client()->DemoSliceEnd();
672 if(CurrentTick < (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick))
673 g_Config.m_ClDemoSliceBegin = -1;
674 }
675 else if(SliceEndButtonResult == 2)
676 {
677 g_Config.m_ClDemoSliceEnd = -1;
678 }
679 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceEndButton, pNearRect: &Button, pText: Localize(pStr: "Mark the end of a cut (right click to reset)"));
680
681 // slice save button
682#if defined(CONF_VIDEORECORDER)
683 const bool SliceEnabled = IVideo::Current() == nullptr;
684#else
685 const bool SliceEnabled = true;
686#endif
687 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
688 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
689 static CButtonContainer s_SliceSaveButton;
690 if(DoButton_FontIcon(pButtonContainer: &s_SliceSaveButton, pText: FONT_ICON_ARROW_UP_RIGHT_FROM_SQUARE, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL, Enabled: SliceEnabled) && SliceEnabled)
691 {
692 char aDemoName[IO_MAX_PATH_LENGTH];
693 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
694 m_DemoSliceInput.Set(aDemoName);
695 Ui()->SetActiveItem(&m_DemoSliceInput);
696 m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE;
697 }
698 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceSaveButton, pNearRect: &Button, pText: Localize(pStr: "Export cut as a separate demo"));
699
700 // close button
701 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
702 static CButtonContainer s_ExitButton;
703 if(DoButton_FontIcon(pButtonContainer: &s_ExitButton, pText: FONT_ICON_XMARK, Checked: 0, pRect: &Button) || (Input()->KeyPress(Key: KEY_C) && m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE))
704 {
705 Client()->Disconnect();
706 DemolistOnUpdate(Reset: false);
707 }
708 GameClient()->m_Tooltips.DoToolTip(pId: &s_ExitButton, pNearRect: &Button, pText: Localize(pStr: "Close the demo player"));
709
710 // toggle keyboard shortcuts button
711 ButtonBar.VSplitRight(Cut: Margins, pLeft: &ButtonBar, pRight: nullptr);
712 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
713 static CButtonContainer s_KeyboardShortcutsButton;
714 if(DoButton_FontIcon(pButtonContainer: &s_KeyboardShortcutsButton, pText: FONT_ICON_KEYBOARD, Checked: 0, pRect: &Button, Corners: IGraphics::CORNER_ALL, Enabled: g_Config.m_ClDemoKeyboardShortcuts != 0))
715 {
716 g_Config.m_ClDemoKeyboardShortcuts ^= 1;
717 }
718 GameClient()->m_Tooltips.DoToolTip(pId: &s_KeyboardShortcutsButton, pNearRect: &Button, pText: Localize(pStr: "Toggle keyboard shortcuts"));
719
720 // demo name
721 char aDemoName[IO_MAX_PATH_LENGTH];
722 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
723 char aBuf[IO_MAX_PATH_LENGTH + 128];
724 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Demofile: %s"), aDemoName);
725 SLabelProperties Props;
726 Props.m_MaxWidth = NameBar.w;
727 Props.m_EllipsisAtEnd = true;
728 Props.m_EnableWidthCheck = false;
729 Ui()->DoLabel(pRect: &NameBar, pText: aBuf, Size: Button.h * 0.5f, Align: TEXTALIGN_ML, LabelProps: Props);
730
731 if(IncreaseDemoSpeed)
732 {
733 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
734 UpdateLastSpeedChange();
735 }
736 else if(DecreaseDemoSpeed)
737 {
738 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
739 UpdateLastSpeedChange();
740 }
741
742 HandleDemoSeeking(PositionToSeek, TimeToSeek);
743
744 // render popups
745 if(m_DemoPlayerState != DEMOPLAYER_NONE)
746 {
747 // prevent element under the active popup from being activated
748 Ui()->SetHotItem(nullptr);
749 }
750 if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE)
751 {
752 RenderDemoPlayerSliceSavePopup(MainView);
753 }
754}
755
756void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView)
757{
758 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
759
760 CUIRect Box;
761 MainView.Margin(Cut: 150.0f, pOtherRect: &Box);
762
763 // background
764 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
765 Box.Margin(Cut: 24.0f, pOtherRect: &Box);
766
767 // title
768 CUIRect Title;
769 Box.HSplitTop(Cut: 24.0f, pTop: &Title, pBottom: &Box);
770 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
771 Ui()->DoLabel(pRect: &Title, pText: Localize(pStr: "Export demo cut"), Size: 24.0f, Align: TEXTALIGN_MC);
772
773 // slice times
774 CUIRect SliceTimesBar, SliceInterval, SliceLength;
775 Box.HSplitTop(Cut: 24.0f, pTop: &SliceTimesBar, pBottom: &Box);
776 SliceTimesBar.VSplitMid(pLeft: &SliceInterval, pRight: &SliceLength, Spacing: 40.0f);
777 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
778 const int64_t RealSliceBegin = g_Config.m_ClDemoSliceBegin == -1 ? 0 : (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick);
779 const int64_t RealSliceEnd = (g_Config.m_ClDemoSliceEnd == -1 ? pInfo->m_LastTick : g_Config.m_ClDemoSliceEnd) - pInfo->m_FirstTick;
780 char aSliceBegin[32];
781 str_time(centisecs: RealSliceBegin / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceBegin, buffer_size: sizeof(aSliceBegin));
782 char aSliceEnd[32];
783 str_time(centisecs: RealSliceEnd / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceEnd, buffer_size: sizeof(aSliceEnd));
784 char aSliceLength[32];
785 str_time(centisecs: (RealSliceEnd - RealSliceBegin) / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceLength, buffer_size: sizeof(aSliceLength));
786 char aBuf[256];
787 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s – %s", Localize(pStr: "Cut interval"), aSliceBegin, aSliceEnd);
788 Ui()->DoLabel(pRect: &SliceInterval, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
789 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Cut length"), aSliceLength);
790 Ui()->DoLabel(pRect: &SliceLength, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
791
792 // file name
793 CUIRect NameLabel, NameBox;
794 Box.HSplitTop(Cut: 24.0f, pTop: &NameLabel, pBottom: &Box);
795 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
796 NameLabel.VSplitLeft(Cut: 150.0f, pLeft: &NameLabel, pRight: &NameBox);
797 NameBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &NameBox);
798 Ui()->DoLabel(pRect: &NameLabel, pText: Localize(pStr: "New name:"), Size: 18.0f, Align: TEXTALIGN_ML);
799 Ui()->DoEditBox(pLineInput: &m_DemoSliceInput, pRect: &NameBox, FontSize: 12.0f);
800
801 // remove chat checkbox
802 static int s_RemoveChat = 0;
803
804 CUIRect CheckBoxBar, RemoveChatCheckBox, RenderCutCheckBox;
805 Box.HSplitTop(Cut: 24.0f, pTop: &CheckBoxBar, pBottom: &Box);
806 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
807 CheckBoxBar.VSplitMid(pLeft: &RemoveChatCheckBox, pRight: &RenderCutCheckBox, Spacing: 40.0f);
808 if(DoButton_CheckBox(pId: &s_RemoveChat, pText: Localize(pStr: "Remove chat"), Checked: s_RemoveChat, pRect: &RemoveChatCheckBox))
809 {
810 s_RemoveChat ^= 1;
811 }
812#if defined(CONF_VIDEORECORDER)
813 static int s_RenderCut = 0;
814 if(DoButton_CheckBox(pId: &s_RenderCut, pText: Localize(pStr: "Render cut to video"), Checked: s_RenderCut, pRect: &RenderCutCheckBox))
815 {
816 s_RenderCut ^= 1;
817 }
818#endif
819
820 // buttons
821 CUIRect ButtonBar, AbortButton, OkButton;
822 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
823 ButtonBar.VSplitMid(pLeft: &AbortButton, pRight: &OkButton, Spacing: 40.0f);
824
825 static CButtonContainer s_ButtonAbort;
826 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &AbortButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
827 m_DemoPlayerState = DEMOPLAYER_NONE;
828
829 static CUi::SConfirmPopupContext s_ConfirmPopupContext;
830 static CButtonContainer s_ButtonOk;
831 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &OkButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
832 {
833 if(str_endswith(str: m_DemoSliceInput.GetString(), suffix: ".demo"))
834 {
835 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
836 fs_split_file_extension(filename: m_DemoSliceInput.GetString(), name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
837 m_DemoSliceInput.Set(aNameWithoutExt);
838 }
839
840 char aDemoName[IO_MAX_PATH_LENGTH];
841 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
842 if(str_comp(a: aDemoName, b: m_DemoSliceInput.GetString()) == 0)
843 {
844 static CUi::SMessagePopupContext s_MessagePopupContext;
845 s_MessagePopupContext.ErrorColor();
846 str_copy(dst&: s_MessagePopupContext.m_aMessage, src: Localize(pStr: "Please use a different filename"));
847 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_MessagePopupContext);
848 }
849 else
850 {
851 char aPath[IO_MAX_PATH_LENGTH];
852 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
853 if(Storage()->FileExists(pFilename: aPath, Type: IStorage::TYPE_SAVE))
854 {
855 s_ConfirmPopupContext.Reset();
856 s_ConfirmPopupContext.YesNoButtons();
857 str_copy(dst&: s_ConfirmPopupContext.m_aMessage, src: Localize(pStr: "File already exists, do you want to overwrite it?"));
858 Ui()->ShowPopupConfirm(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_ConfirmPopupContext);
859 }
860 else
861 s_ConfirmPopupContext.m_Result = CUi::SConfirmPopupContext::CONFIRMED;
862 }
863 }
864
865 if(s_ConfirmPopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED)
866 {
867 char aPath[IO_MAX_PATH_LENGTH];
868 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
869 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_DemoSliceInput.GetString());
870 if(str_endswith(str: m_aCurrentDemoSelectionName, suffix: ".demo"))
871 m_aCurrentDemoSelectionName[str_length(str: m_aCurrentDemoSelectionName) - str_length(str: ".demo")] = '\0';
872
873 Client()->DemoSlice(pDstPath: aPath, pfnFilter: CMenus::DemoFilterChat, pUser: &s_RemoveChat);
874 DemolistPopulate();
875 DemolistOnUpdate(Reset: false);
876 m_DemoPlayerState = DEMOPLAYER_NONE;
877#if defined(CONF_VIDEORECORDER)
878 if(s_RenderCut)
879 {
880 m_Popup = POPUP_RENDER_DEMO;
881 m_StartPaused = false;
882 m_DemoRenderInput.Set(m_aCurrentDemoSelectionName);
883 Ui()->SetActiveItem(&m_DemoRenderInput);
884 if(m_DemolistStorageType != IStorage::TYPE_ALL && m_DemolistStorageType != IStorage::TYPE_SAVE)
885 m_DemolistStorageType = IStorage::TYPE_ALL; // Select a storage type containing the sliced demo
886 }
887#endif
888 }
889 if(s_ConfirmPopupContext.m_Result != CUi::SConfirmPopupContext::UNSET)
890 {
891 s_ConfirmPopupContext.Reset();
892 }
893}
894
895int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
896{
897 CMenus *pSelf = (CMenus *)pUser;
898 if(str_comp(a: pInfo->m_pName, b: ".") == 0 ||
899 (str_comp(a: pInfo->m_pName, b: "..") == 0 && (pSelf->m_aCurrentDemoFolder[0] == '\0' || (!pSelf->m_DemolistMultipleStorages && str_comp(a: pSelf->m_aCurrentDemoFolder, b: "demos") == 0))) ||
900 (!IsDir && !str_endswith(str: pInfo->m_pName, suffix: ".demo")))
901 {
902 return 0;
903 }
904
905 CDemoItem Item;
906 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
907 if(IsDir)
908 {
909 str_format(buffer: Item.m_aName, buffer_size: sizeof(Item.m_aName), format: "%s/", pInfo->m_pName);
910 Item.m_Date = 0;
911 }
912 else
913 {
914 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - str_length(str: ".demo"));
915 Item.m_Date = pInfo->m_TimeModified;
916 }
917 Item.m_InfosLoaded = false;
918 Item.m_Valid = false;
919 Item.m_IsDir = IsDir != 0;
920 Item.m_IsLink = false;
921 Item.m_StorageType = StorageType;
922 pSelf->m_vDemos.push_back(x: Item);
923
924 if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms)
925 {
926 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading demo files"), pContent: "", IncreaseCounter: 0, RenderLoadingBar: false);
927 }
928
929 return 0;
930}
931
932void CMenus::DemolistPopulate()
933{
934 m_vDemos.clear();
935
936 int NumStoragesWithDemos = 0;
937 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
938 {
939 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
940 {
941 NumStoragesWithDemos++;
942 }
943 }
944 m_DemolistMultipleStorages = NumStoragesWithDemos > 1;
945
946 if(m_aCurrentDemoFolder[0] == '\0')
947 {
948 {
949 CDemoItem Item;
950 str_copy(dst&: Item.m_aFilename, src: "demos");
951 str_copy(dst&: Item.m_aName, src: Localize(pStr: "All combined"));
952 Item.m_InfosLoaded = false;
953 Item.m_Valid = false;
954 Item.m_Date = 0;
955 Item.m_IsDir = true;
956 Item.m_IsLink = true;
957 Item.m_StorageType = IStorage::TYPE_ALL;
958 m_vDemos.push_back(x: Item);
959 }
960
961 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
962 {
963 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
964 {
965 CDemoItem Item;
966 str_copy(dst&: Item.m_aFilename, src: "demos");
967 Storage()->GetCompletePath(Type: StorageType, pDir: "demos", pBuffer: Item.m_aName, BufferSize: sizeof(Item.m_aName));
968 str_append(dst: Item.m_aName, src: "/", dst_size: sizeof(Item.m_aName));
969 Item.m_InfosLoaded = false;
970 Item.m_Valid = false;
971 Item.m_Date = 0;
972 Item.m_IsDir = true;
973 Item.m_IsLink = true;
974 Item.m_StorageType = StorageType;
975 m_vDemos.push_back(x: Item);
976 }
977 }
978 }
979 else
980 {
981 m_DemoPopulateStartTime = time_get_nanoseconds();
982 Storage()->ListDirectoryInfo(Type: m_DemolistStorageType, pPath: m_aCurrentDemoFolder, pfnCallback: DemolistFetchCallback, pUser: this);
983
984 if(g_Config.m_BrDemoFetchInfo)
985 FetchAllHeaders();
986
987 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
988 }
989 RefreshFilteredDemos();
990}
991
992void CMenus::RefreshFilteredDemos()
993{
994 m_vpFilteredDemos.clear();
995 for(auto &Demo : m_vDemos)
996 {
997 if(str_find_nocase(haystack: Demo.m_aFilename, needle: m_DemoSearchInput.GetString()))
998 {
999 m_vpFilteredDemos.push_back(x: &Demo);
1000 }
1001 }
1002}
1003
1004void CMenus::DemolistOnUpdate(bool Reset)
1005{
1006 if(Reset)
1007 {
1008 if(m_vpFilteredDemos.empty())
1009 {
1010 m_DemolistSelectedIndex = -1;
1011 m_aCurrentDemoSelectionName[0] = '\0';
1012 }
1013 else
1014 {
1015 m_DemolistSelectedIndex = 0;
1016 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1017 }
1018 }
1019 else
1020 {
1021 RefreshFilteredDemos();
1022
1023 // search for selected index
1024 m_DemolistSelectedIndex = -1;
1025 int SelectedIndex = -1;
1026 for(const auto &pItem : m_vpFilteredDemos)
1027 {
1028 SelectedIndex++;
1029 if(str_comp(a: m_aCurrentDemoSelectionName, b: pItem->m_aName) == 0)
1030 {
1031 m_DemolistSelectedIndex = SelectedIndex;
1032 break;
1033 }
1034 }
1035 }
1036
1037 if(m_DemolistSelectedIndex >= 0)
1038 m_DemolistSelectedReveal = true;
1039}
1040
1041bool CMenus::FetchHeader(CDemoItem &Item)
1042{
1043 if(!Item.m_InfosLoaded)
1044 {
1045 char aBuffer[IO_MAX_PATH_LENGTH];
1046 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename);
1047 Item.m_Valid = DemoPlayer()->GetDemoInfo(pStorage: Storage(), pConsole: nullptr, pFilename: aBuffer, StorageType: Item.m_StorageType, pDemoHeader: &Item.m_Info, pTimelineMarkers: &Item.m_TimelineMarkers, pMapInfo: &Item.m_MapInfo);
1048 Item.m_InfosLoaded = true;
1049 }
1050 return Item.m_Valid;
1051}
1052
1053void CMenus::FetchAllHeaders()
1054{
1055 for(auto &Item : m_vDemos)
1056 {
1057 FetchHeader(Item);
1058 }
1059 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1060}
1061
1062void CMenus::RenderDemoBrowser(CUIRect MainView)
1063{
1064 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_DEMOS);
1065
1066 CUIRect ListView, DetailsView, ButtonsView;
1067 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1068 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1069 MainView.HSplitBottom(Cut: 22.0f * 2.0f + 5.0f, pTop: &ListView, pBottom: &ButtonsView);
1070 ListView.VSplitRight(Cut: 205.0f, pLeft: &ListView, pRight: &DetailsView);
1071 ListView.VSplitRight(Cut: 5.0f, pLeft: &ListView, pRight: nullptr);
1072
1073 bool WasListboxItemActivated;
1074 RenderDemoBrowserList(ListView, WasListboxItemActivated);
1075 RenderDemoBrowserDetails(DetailsView);
1076 RenderDemoBrowserButtons(ButtonsView, WasListboxItemActivated);
1077}
1078
1079void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivated)
1080{
1081 if(!m_DemoBrowserListInitialized)
1082 {
1083 DemolistPopulate();
1084 DemolistOnUpdate(Reset: true);
1085 m_DemoBrowserListInitialized = true;
1086 }
1087
1088#if defined(CONF_VIDEORECORDER)
1089 if(!m_DemoRenderInput.IsEmpty())
1090 {
1091 if(DemoPlayer()->ErrorMessage()[0] == '\0')
1092 {
1093 m_Popup = POPUP_RENDER_DONE;
1094 }
1095 else
1096 {
1097 m_DemoRenderInput.Clear();
1098 }
1099 }
1100#endif
1101
1102 struct SColumn
1103 {
1104 int m_Id;
1105 int m_Sort;
1106 const char *m_pCaption;
1107 int m_Direction;
1108 float m_Width;
1109 CUIRect m_Rect;
1110 };
1111
1112 enum
1113 {
1114 COL_ICON = 0,
1115 COL_DEMONAME,
1116 COL_LENGTH,
1117 COL_DATE,
1118 };
1119
1120 static CListBox s_ListBox;
1121 static SColumn s_aCols[] = {
1122 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1123 {.m_Id: COL_ICON, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: ms_ListheaderHeight, .m_Rect: {.x: 0}},
1124 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1125 {.m_Id: COL_DEMONAME, .m_Sort: SORT_DEMONAME, .m_pCaption: Localizable(pStr: "Demo"), .m_Direction: 0, .m_Width: 0.0f, .m_Rect: {.x: 0}},
1126 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1127 {.m_Id: COL_LENGTH, .m_Sort: SORT_LENGTH, .m_pCaption: Localizable(pStr: "Length"), .m_Direction: 1, .m_Width: 75.0f, .m_Rect: {.x: 0}},
1128 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
1129 {.m_Id: COL_DATE, .m_Sort: SORT_DATE, .m_pCaption: Localizable(pStr: "Date"), .m_Direction: 1, .m_Width: 150.0f, .m_Rect: {.x: 0}},
1130 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_Width: s_ListBox.ScrollbarWidthMax(), .m_Rect: {.x: 0}},
1131 };
1132
1133 CUIRect Headers, ListBox;
1134 ListView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &ListBox);
1135 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1136 ListBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1137
1138 for(auto &Col : s_aCols)
1139 {
1140 if(Col.m_Direction == -1)
1141 {
1142 Headers.VSplitLeft(Cut: Col.m_Width, pLeft: &Col.m_Rect, pRight: &Headers);
1143 }
1144 }
1145
1146 for(int i = std::size(s_aCols) - 1; i >= 0; i--)
1147 {
1148 if(s_aCols[i].m_Direction == 1)
1149 {
1150 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
1151 }
1152 }
1153
1154 for(auto &Col : s_aCols)
1155 {
1156 if(Col.m_Direction == 0)
1157 Col.m_Rect = Headers;
1158 }
1159
1160 for(auto &Col : s_aCols)
1161 {
1162 if(Col.m_pCaption[0] != '\0' && Col.m_Sort != -1)
1163 {
1164 if(DoButton_GridHeader(pId: &Col.m_Id, pText: Localize(pStr: Col.m_pCaption), Checked: g_Config.m_BrDemoSort == Col.m_Sort, pRect: &Col.m_Rect))
1165 {
1166 if(g_Config.m_BrDemoSort == Col.m_Sort)
1167 g_Config.m_BrDemoSortOrder ^= 1;
1168 else
1169 g_Config.m_BrDemoSortOrder = 0;
1170 g_Config.m_BrDemoSort = Col.m_Sort;
1171 // Don't rescan in order to keep fetched headers, just resort
1172 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1173 DemolistOnUpdate(Reset: false);
1174 }
1175 }
1176 }
1177
1178 if(m_DemolistSelectedReveal)
1179 {
1180 s_ListBox.ScrollToSelected();
1181 m_DemolistSelectedReveal = false;
1182 }
1183
1184 s_ListBox.DoStart(RowHeight: ms_ListheaderHeight, NumItems: m_vpFilteredDemos.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: m_DemolistSelectedIndex, pRect: &ListBox, Background: false, BackgroundCorners: IGraphics::CORNER_ALL, ForceShowScrollbar: true);
1185
1186 char aBuf[64];
1187 int ItemIndex = -1;
1188 for(auto &pItem : m_vpFilteredDemos)
1189 {
1190 ItemIndex++;
1191
1192 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: ItemIndex == m_DemolistSelectedIndex);
1193 if(!ListItem.m_Visible)
1194 continue;
1195
1196 for(const auto &Col : s_aCols)
1197 {
1198 CUIRect Button;
1199 Button.x = Col.m_Rect.x;
1200 Button.y = ListItem.m_Rect.y;
1201 Button.h = ListItem.m_Rect.h;
1202 Button.w = Col.m_Rect.w;
1203
1204 if(Col.m_Id == COL_ICON)
1205 {
1206 Button.Margin(Cut: 1.0f, pOtherRect: &Button);
1207
1208 const char *pIconType;
1209 if(pItem->m_IsLink || str_comp(a: pItem->m_aFilename, b: "..") == 0)
1210 pIconType = FONT_ICON_FOLDER_TREE;
1211 else if(pItem->m_IsDir)
1212 pIconType = FONT_ICON_FOLDER;
1213 else
1214 pIconType = FONT_ICON_FILM;
1215
1216 ColorRGBA IconColor;
1217 if(!pItem->m_IsDir && (!pItem->m_InfosLoaded || !pItem->m_Valid))
1218 IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded
1219 else
1220 IconColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
1221
1222 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1223 TextRender()->TextColor(rgb: IconColor);
1224 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1225 Ui()->DoLabel(pRect: &Button, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
1226 TextRender()->SetRenderFlags(0);
1227 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1228 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1229 }
1230 else if(Col.m_Id == COL_DEMONAME)
1231 {
1232 SLabelProperties Props;
1233 Props.m_MaxWidth = Button.w;
1234 Props.m_EllipsisAtEnd = true;
1235 Props.m_EnableWidthCheck = false;
1236 Ui()->DoLabel(pRect: &Button, pText: pItem->m_aName, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1237 }
1238 else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid)
1239 {
1240 str_time(centisecs: (int64_t)pItem->Length() * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1241 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1242 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1243 }
1244 else if(Col.m_Id == COL_DATE && !pItem->m_IsDir)
1245 {
1246 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), FORMAT_SPACE);
1247 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1248 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1249 }
1250 }
1251 }
1252
1253 const int NewSelected = s_ListBox.DoEnd();
1254 if(NewSelected != m_DemolistSelectedIndex)
1255 {
1256 m_DemolistSelectedIndex = NewSelected;
1257 if(m_DemolistSelectedIndex >= 0)
1258 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1259 DemolistOnUpdate(Reset: false);
1260 }
1261
1262 WasListboxItemActivated = s_ListBox.WasItemActivated();
1263}
1264
1265void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView)
1266{
1267 CUIRect Contents, Header;
1268 DetailsView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &Contents);
1269 Contents.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1270 Contents.Margin(Cut: 5.0f, pOtherRect: &Contents);
1271
1272 const float FontSize = 12.0f;
1273 CDemoItem *pItem = m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex] : nullptr;
1274
1275 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1276 const char *pHeaderLabel;
1277 if(pItem == nullptr)
1278 pHeaderLabel = Localize(pStr: "No demo selected");
1279 else if(str_comp(a: pItem->m_aFilename, b: "..") == 0)
1280 pHeaderLabel = Localize(pStr: "Parent Folder");
1281 else if(pItem->m_IsLink)
1282 pHeaderLabel = Localize(pStr: "Folder Link");
1283 else if(pItem->m_IsDir)
1284 pHeaderLabel = Localize(pStr: "Folder");
1285 else if(!FetchHeader(Item&: *pItem))
1286 pHeaderLabel = Localize(pStr: "Invalid Demo");
1287 else
1288 pHeaderLabel = Localize(pStr: "Demo");
1289 Ui()->DoLabel(pRect: &Header, pText: pHeaderLabel, Size: FontSize + 2.0f, Align: TEXTALIGN_MC);
1290
1291 if(pItem == nullptr || pItem->m_IsDir)
1292 return;
1293
1294 char aBuf[256];
1295 CUIRect Left, Right;
1296
1297 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1298 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Created"), Size: FontSize, Align: TEXTALIGN_ML);
1299 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), FORMAT_SPACE);
1300 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1301 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1302 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1303
1304 if(!pItem->m_Valid)
1305 return;
1306
1307 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1308 Left.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 4.0f);
1309 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Type"), Size: FontSize, Align: TEXTALIGN_ML);
1310 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1311 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1312 Left.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 4.0f);
1313 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aType, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1314 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->m_Info.m_Version);
1315 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1316 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1317
1318 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1319 Left.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 4.0f);
1320 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Length"), Size: FontSize, Align: TEXTALIGN_ML);
1321 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Markers"), Size: FontSize, Align: TEXTALIGN_ML);
1322 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1323 Left.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 4.0f);
1324 str_time(centisecs: (int64_t)pItem->Length() * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1325 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1326 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1327 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1328 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1329
1330 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1331 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Netversion"), Size: FontSize, Align: TEXTALIGN_ML);
1332 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1333 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aNetversion, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1334 Contents.HSplitTop(Cut: 16.0f, pTop: nullptr, pBottom: &Contents);
1335
1336 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1337 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map"), Size: FontSize, Align: TEXTALIGN_ML);
1338 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1339 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aMapName, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1340 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1341
1342 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1343 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Size"), Size: FontSize, Align: TEXTALIGN_ML);
1344 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1345 const float Size = pItem->Size() / 1024.0f;
1346 if(Size == 0.0f)
1347 str_copy(dst&: aBuf, src: Localize(pStr: "map not included", pContext: "Demo details"));
1348 else if(Size > 1024)
1349 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), Size / 1024.0f);
1350 else
1351 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), Size);
1352 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1353 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1354
1355 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1356 if(pItem->m_MapInfo.m_Sha256 != SHA256_ZEROED)
1357 {
1358 Ui()->DoLabel(pRect: &Left, pText: "SHA256", Size: FontSize, Align: TEXTALIGN_ML);
1359 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1360 char aSha[SHA256_MAXSTRSIZE];
1361 sha256_str(digest: pItem->m_MapInfo.m_Sha256, str: aSha, max_len: sizeof(aSha));
1362 SLabelProperties Props;
1363 Props.m_MaxWidth = Left.w;
1364 Props.m_EllipsisAtEnd = true;
1365 Props.m_EnableWidthCheck = false;
1366 Ui()->DoLabel(pRect: &Left, pText: aSha, Size: FontSize - 1.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1367 }
1368 else
1369 {
1370 Ui()->DoLabel(pRect: &Left, pText: "CRC32", Size: FontSize, Align: TEXTALIGN_ML);
1371 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1372 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%08x", pItem->m_MapInfo.m_Crc);
1373 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1374 }
1375 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1376}
1377
1378void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemActivated)
1379{
1380 const auto &&SetIconMode = [&](bool Enable) {
1381 if(Enable)
1382 {
1383 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1384 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1385 }
1386 else
1387 {
1388 TextRender()->SetRenderFlags(0);
1389 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1390 }
1391 };
1392
1393 CUIRect ButtonBarTop, ButtonBarBottom;
1394 ButtonsView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ButtonsView);
1395 ButtonsView.HSplitMid(pTop: &ButtonBarTop, pBottom: &ButtonBarBottom, Spacing: 5.0f);
1396
1397 // quick search
1398 {
1399 SetIconMode(true);
1400 CUIRect DemoSearch, SearchIcon;
1401 ButtonBarTop.VSplitLeft(Cut: ButtonBarBottom.h * 21.0f, pLeft: &DemoSearch, pRight: &ButtonBarTop);
1402 ButtonBarTop.VSplitLeft(Cut: ButtonBarTop.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarTop);
1403 DemoSearch.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: FONT_ICON_MAGNIFYING_GLASS), pLeft: &SearchIcon, pRight: &DemoSearch);
1404 DemoSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &DemoSearch);
1405 Ui()->DoLabel(pRect: &SearchIcon, pText: FONT_ICON_MAGNIFYING_GLASS, Size: 14.0f, Align: TEXTALIGN_ML);
1406 SetIconMode(false);
1407 m_DemoSearchInput.SetEmptyText(Localize(pStr: "Search"));
1408
1409 if(Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
1410 {
1411 Ui()->SetActiveItem(&m_DemoSearchInput);
1412 m_DemoSearchInput.SelectAll();
1413 }
1414 if(Ui()->DoClearableEditBox(pLineInput: &m_DemoSearchInput, pRect: &DemoSearch, FontSize: 12.0f))
1415 {
1416 RefreshFilteredDemos();
1417 DemolistOnUpdate(Reset: false);
1418 }
1419 }
1420
1421 // refresh button
1422 {
1423 CUIRect RefreshButton;
1424 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 3.0f, pLeft: &RefreshButton, pRight: &ButtonBarBottom);
1425 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1426 SetIconMode(true);
1427 static CButtonContainer s_RefreshButton;
1428 if(DoButton_Menu(pButtonContainer: &s_RefreshButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
1429 {
1430 SetIconMode(false);
1431 DemolistPopulate();
1432 DemolistOnUpdate(Reset: false);
1433 }
1434 SetIconMode(false);
1435 }
1436
1437 // fetch info checkbox
1438 {
1439 CUIRect FetchInfo;
1440 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 7.0f, pLeft: &FetchInfo, pRight: &ButtonBarBottom);
1441 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1442 if(DoButton_CheckBox(pId: &g_Config.m_BrDemoFetchInfo, pText: Localize(pStr: "Fetch Info"), Checked: g_Config.m_BrDemoFetchInfo, pRect: &FetchInfo))
1443 {
1444 g_Config.m_BrDemoFetchInfo ^= 1;
1445 if(g_Config.m_BrDemoFetchInfo)
1446 FetchAllHeaders();
1447 }
1448 }
1449
1450 // demos directory button
1451 if(m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
1452 {
1453 CUIRect DemosDirectoryButton;
1454 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 10.0f, pLeft: &DemosDirectoryButton, pRight: &ButtonBarBottom);
1455 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1456 static CButtonContainer s_DemosDirectoryButton;
1457 if(DoButton_Menu(pButtonContainer: &s_DemosDirectoryButton, pText: Localize(pStr: "Demos directory"), Checked: 0, pRect: &DemosDirectoryButton))
1458 {
1459 char aBuf[IO_MAX_PATH_LENGTH];
1460 Storage()->GetCompletePath(Type: m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, pDir: m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1461 Client()->ViewFile(pFilename: aBuf);
1462 }
1463 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemosDirectoryButton, pNearRect: &DemosDirectoryButton, pText: Localize(pStr: "Open the directory that contains the demo files"));
1464 }
1465
1466 // play/open button
1467 if(m_DemolistSelectedIndex >= 0)
1468 {
1469 CUIRect PlayButton;
1470 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &PlayButton);
1471 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarBottom, pRight: nullptr);
1472 SetIconMode(true);
1473 static CButtonContainer s_PlayButton;
1474 if(DoButton_Menu(pButtonContainer: &s_PlayButton, pText: (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FONT_ICON_FOLDER_OPEN : FONT_ICON_PLAY, Checked: 0, pRect: &PlayButton) || WasListboxItemActivated || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || (Input()->KeyPress(Key: KEY_P) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
1475 {
1476 SetIconMode(false);
1477 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
1478 {
1479 m_DemoSearchInput.Clear();
1480 const bool ParentFolder = str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") == 0;
1481 if(ParentFolder) // parent folder
1482 {
1483 str_copy(dst&: m_aCurrentDemoSelectionName, src: fs_filename(path: m_aCurrentDemoFolder));
1484 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1485 if(fs_parent_dir(path: m_aCurrentDemoFolder))
1486 {
1487 m_aCurrentDemoFolder[0] = '\0';
1488 if(m_DemolistStorageType == IStorage::TYPE_ALL)
1489 {
1490 m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item
1491 }
1492 else
1493 {
1494 Storage()->GetCompletePath(Type: m_DemolistStorageType, pDir: "demos", pBuffer: m_aCurrentDemoSelectionName, BufferSize: sizeof(m_aCurrentDemoSelectionName));
1495 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1496 }
1497 }
1498 }
1499 else // sub folder
1500 {
1501 if(m_aCurrentDemoFolder[0] != '\0')
1502 str_append(dst&: m_aCurrentDemoFolder, src: "/");
1503 else
1504 m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
1505 str_append(dst&: m_aCurrentDemoFolder, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1506 }
1507 DemolistPopulate();
1508 DemolistOnUpdate(Reset: !ParentFolder);
1509 }
1510 else // file
1511 {
1512 char aBuf[IO_MAX_PATH_LENGTH];
1513 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1514 const char *pError = Client()->DemoPlayer_Play(pFilename: aBuf, StorageType: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType);
1515 m_LastPauseChange = -1.0f;
1516 m_LastSpeedChange = -1.0f;
1517 if(pError)
1518 {
1519 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
1520 }
1521 else
1522 {
1523 Ui()->SetActiveItem(nullptr);
1524 return;
1525 }
1526 }
1527 }
1528 SetIconMode(false);
1529
1530 if(m_aCurrentDemoFolder[0] != '\0')
1531 {
1532 if(str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
1533 {
1534 // rename button
1535 CUIRect RenameButton;
1536 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &RenameButton);
1537 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1538 SetIconMode(true);
1539 static CButtonContainer s_RenameButton;
1540 if(DoButton_Menu(pButtonContainer: &s_RenameButton, pText: FONT_ICON_PENCIL, Checked: 0, pRect: &RenameButton))
1541 {
1542 SetIconMode(false);
1543 m_Popup = POPUP_RENAME_DEMO;
1544 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1545 {
1546 m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1547 }
1548 else
1549 {
1550 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1551 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1552 m_DemoRenameInput.Set(aNameWithoutExt);
1553 }
1554 Ui()->SetActiveItem(&m_DemoRenameInput);
1555 return;
1556 }
1557
1558 // delete button
1559 static CButtonContainer s_DeleteButton;
1560 CUIRect DeleteButton;
1561 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &DeleteButton);
1562 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1563 if(DoButton_Menu(pButtonContainer: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: 0, pRect: &DeleteButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE) || (Input()->KeyPress(Key: KEY_D) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
1564 {
1565 SetIconMode(false);
1566 char aBuf[128 + IO_MAX_PATH_LENGTH];
1567 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Are you sure that you want to delete the folder '%s'?") : Localize(pStr: "Are you sure that you want to delete the demo '%s'?"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1568 PopupConfirm(pTitle: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Delete folder") : Localize(pStr: "Delete demo"), pMessage: aBuf, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? &CMenus::PopupConfirmDeleteFolder : &CMenus::PopupConfirmDeleteDemo);
1569 return;
1570 }
1571 SetIconMode(false);
1572 }
1573
1574#if defined(CONF_VIDEORECORDER)
1575 // render demo button
1576 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1577 {
1578 CUIRect RenderButton;
1579 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarTop, pRight: &RenderButton);
1580 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarTop, pRight: nullptr);
1581 SetIconMode(true);
1582 static CButtonContainer s_RenderButton;
1583 if(DoButton_Menu(pButtonContainer: &s_RenderButton, pText: FONT_ICON_VIDEO, Checked: 0, pRect: &RenderButton) || (Input()->KeyPress(Key: KEY_R) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
1584 {
1585 SetIconMode(false);
1586 m_Popup = POPUP_RENDER_DEMO;
1587 m_StartPaused = false;
1588 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1589 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1590 m_DemoRenderInput.Set(aNameWithoutExt);
1591 Ui()->SetActiveItem(&m_DemoRenderInput);
1592 return;
1593 }
1594 SetIconMode(false);
1595 }
1596#endif
1597 }
1598 }
1599}
1600
1601void CMenus::PopupConfirmDeleteDemo()
1602{
1603 char aBuf[IO_MAX_PATH_LENGTH];
1604 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1605 if(Storage()->RemoveFile(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1606 {
1607 DemolistPopulate();
1608 DemolistOnUpdate(Reset: false);
1609 }
1610 else
1611 {
1612 char aError[128 + IO_MAX_PATH_LENGTH];
1613 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the demo '%s'"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1614 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1615 }
1616}
1617
1618void CMenus::PopupConfirmDeleteFolder()
1619{
1620 char aBuf[IO_MAX_PATH_LENGTH];
1621 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1622 if(Storage()->RemoveFolder(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1623 {
1624 DemolistPopulate();
1625 DemolistOnUpdate(Reset: false);
1626 }
1627 else
1628 {
1629 char aError[128 + IO_MAX_PATH_LENGTH];
1630 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the folder '%s'. Make sure it's empty first."), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1631 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1632 }
1633}
1634