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