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 if(g_Config.m_BrDemoFetchInfo)
1012 FetchAllHeaders();
1013
1014 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1015 }
1016 RefreshFilteredDemos();
1017}
1018
1019void CMenus::RefreshFilteredDemos()
1020{
1021 m_vpFilteredDemos.clear();
1022 for(auto &Demo : m_vDemos)
1023 {
1024 if(str_find_nocase(haystack: Demo.m_aFilename, needle: m_DemoSearchInput.GetString()))
1025 {
1026 m_vpFilteredDemos.push_back(x: &Demo);
1027 }
1028 }
1029}
1030
1031void CMenus::DemolistOnUpdate(bool Reset)
1032{
1033 if(Reset)
1034 {
1035 if(m_vpFilteredDemos.empty())
1036 {
1037 m_DemolistSelectedIndex = -1;
1038 m_aCurrentDemoSelectionName[0] = '\0';
1039 }
1040 else
1041 {
1042 m_DemolistSelectedIndex = 0;
1043 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1044 }
1045 }
1046 else
1047 {
1048 RefreshFilteredDemos();
1049
1050 // search for selected index
1051 m_DemolistSelectedIndex = -1;
1052 int SelectedIndex = -1;
1053 for(const auto &pItem : m_vpFilteredDemos)
1054 {
1055 SelectedIndex++;
1056 if(str_comp(a: m_aCurrentDemoSelectionName, b: pItem->m_aName) == 0)
1057 {
1058 m_DemolistSelectedIndex = SelectedIndex;
1059 break;
1060 }
1061 }
1062 }
1063
1064 if(m_DemolistSelectedIndex >= 0)
1065 m_DemolistSelectedReveal = true;
1066}
1067
1068bool CMenus::FetchHeader(CDemoItem &Item)
1069{
1070 if(!Item.m_InfosLoaded)
1071 {
1072 char aBuffer[IO_MAX_PATH_LENGTH];
1073 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename);
1074 IOHANDLE File;
1075 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);
1076 Item.m_InfosLoaded = true;
1077
1078 if(Item.m_Valid && File)
1079 {
1080 Item.m_Size = io_length(io: File);
1081 io_close(io: File);
1082 }
1083 }
1084 return Item.m_Valid;
1085}
1086
1087void CMenus::FetchAllHeaders()
1088{
1089 for(auto &Item : m_vDemos)
1090 {
1091 FetchHeader(Item);
1092 }
1093 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1094}
1095
1096void CMenus::RenderDemoBrowser(CUIRect MainView)
1097{
1098 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_DEMOS);
1099
1100 CUIRect ListView, DetailsView, ButtonsView;
1101 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1102 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1103 MainView.HSplitBottom(Cut: 22.0f * 2.0f + 5.0f, pTop: &ListView, pBottom: &ButtonsView);
1104 ListView.VSplitRight(Cut: 205.0f, pLeft: &ListView, pRight: &DetailsView);
1105 ListView.VSplitRight(Cut: 5.0f, pLeft: &ListView, pRight: nullptr);
1106
1107 bool WasListboxItemActivated;
1108 RenderDemoBrowserList(ListView, WasListboxItemActivated);
1109 RenderDemoBrowserDetails(DetailsView);
1110 RenderDemoBrowserButtons(ButtonsView, WasListboxItemActivated);
1111}
1112
1113void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivated)
1114{
1115 if(!m_DemoBrowserListInitialized)
1116 {
1117 DemolistPopulate();
1118 DemolistOnUpdate(Reset: true);
1119 m_DemoBrowserListInitialized = true;
1120 }
1121
1122#if defined(CONF_VIDEORECORDER)
1123 if(!m_DemoRenderInput.IsEmpty())
1124 {
1125 if(DemoPlayer()->ErrorMessage()[0] == '\0')
1126 {
1127 m_Popup = POPUP_RENDER_DONE;
1128 }
1129 else
1130 {
1131 m_DemoRenderInput.Clear();
1132 }
1133 }
1134#endif
1135
1136 class CColumn
1137 {
1138 public:
1139 int m_Id;
1140 int m_Sort;
1141 const char *m_pCaption;
1142 int m_Direction;
1143 bool m_FontIcon;
1144 float m_Width;
1145 CUIRect m_Rect;
1146 const char *m_pTooltip;
1147 };
1148
1149 enum
1150 {
1151 COL_ICON = 0,
1152 COL_DEMONAME,
1153 COL_MARKERS,
1154 COL_LENGTH,
1155 COL_DATE,
1156 };
1157
1158 static CListBox s_ListBox;
1159 static CColumn s_aCols[] = {
1160 {.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},
1161 {.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},
1162 {.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},
1163 {.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},
1164 {.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},
1165 {.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")},
1166 {.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},
1167 {.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},
1168 {.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},
1169 {.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},
1170 {.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},
1171 };
1172
1173 CUIRect Headers, ListBox;
1174 ListView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &ListBox);
1175 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1176 ListBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1177
1178 for(auto &Col : s_aCols)
1179 {
1180 if(Col.m_Direction == -1)
1181 {
1182 Headers.VSplitLeft(Cut: Col.m_Width, pLeft: &Col.m_Rect, pRight: &Headers);
1183 }
1184 }
1185
1186 for(int i = std::size(s_aCols) - 1; i >= 0; i--)
1187 {
1188 if(s_aCols[i].m_Direction == 1)
1189 {
1190 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
1191 }
1192 }
1193
1194 for(auto &Col : s_aCols)
1195 {
1196 if(Col.m_Direction == 0)
1197 Col.m_Rect = Headers;
1198 }
1199
1200 for(auto &Col : s_aCols)
1201 {
1202 if(Col.m_pCaption[0] != '\0' && Col.m_Sort != -1)
1203 {
1204 if(Col.m_FontIcon)
1205 {
1206 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1207 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1208 }
1209 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);
1210 if(Col.m_pTooltip != nullptr)
1211 {
1212 GameClient()->m_Tooltips.DoToolTip(pId: &Col.m_Id, pNearRect: &Col.m_Rect, pText: Localize(pStr: Col.m_pTooltip));
1213 }
1214 if(Col.m_FontIcon)
1215 {
1216 TextRender()->SetRenderFlags(0);
1217 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1218 }
1219 if(ButtonPressed)
1220 {
1221 if(g_Config.m_BrDemoSort == Col.m_Sort)
1222 g_Config.m_BrDemoSortOrder ^= 1;
1223 else
1224 g_Config.m_BrDemoSortOrder = 0;
1225 g_Config.m_BrDemoSort = Col.m_Sort;
1226 // Don't rescan in order to keep fetched headers, just resort
1227 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1228 DemolistOnUpdate(Reset: false);
1229 }
1230 }
1231 }
1232
1233 if(m_DemolistSelectedReveal)
1234 {
1235 s_ListBox.ScrollToSelected();
1236 m_DemolistSelectedReveal = false;
1237 }
1238
1239 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);
1240
1241 char aBuf[64];
1242 int ItemIndex = -1;
1243 for(auto &pItem : m_vpFilteredDemos)
1244 {
1245 ItemIndex++;
1246
1247 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: ItemIndex == m_DemolistSelectedIndex);
1248 if(!ListItem.m_Visible)
1249 continue;
1250
1251 for(const auto &Col : s_aCols)
1252 {
1253 CUIRect Button;
1254 Button.x = Col.m_Rect.x;
1255 Button.y = ListItem.m_Rect.y;
1256 Button.h = ListItem.m_Rect.h;
1257 Button.w = Col.m_Rect.w;
1258
1259 if(Col.m_Id == COL_ICON)
1260 {
1261 Button.Margin(Cut: 1.0f, pOtherRect: &Button);
1262
1263 const char *pIconType;
1264 if(pItem->m_IsLink || str_comp(a: pItem->m_aFilename, b: "..") == 0)
1265 pIconType = FontIcon::FOLDER_TREE;
1266 else if(pItem->m_IsDir)
1267 pIconType = FontIcon::FOLDER;
1268 else
1269 pIconType = FontIcon::FILM;
1270
1271 ColorRGBA IconColor;
1272 if(!pItem->m_IsDir && (!pItem->m_InfosLoaded || !pItem->m_Valid))
1273 IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded
1274 else
1275 IconColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
1276
1277 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1278 TextRender()->TextColor(Color: IconColor);
1279 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1280 Ui()->DoLabel(pRect: &Button, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
1281 TextRender()->SetRenderFlags(0);
1282 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1283 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1284 }
1285 else if(Col.m_Id == COL_DEMONAME)
1286 {
1287 SLabelProperties Props;
1288 Props.m_MaxWidth = Button.w;
1289 Props.m_EllipsisAtEnd = true;
1290 Props.m_EnableWidthCheck = false;
1291 Ui()->DoLabel(pRect: &Button, pText: pItem->m_aName, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1292 }
1293 else if(Col.m_Id == COL_MARKERS && !pItem->m_IsDir && pItem->m_Valid)
1294 {
1295 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1296 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1297 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1298 }
1299 else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid)
1300 {
1301 str_time(centisecs: (int64_t)pItem->Length() * 100, format: ETimeFormat::HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1302 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1303 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1304 }
1305 else if(Col.m_Id == COL_DATE && !pItem->m_IsDir)
1306 {
1307 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), format: TimestampFormat::SPACE);
1308 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1309 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1310 }
1311 }
1312 }
1313
1314 const int NewSelected = s_ListBox.DoEnd();
1315 if(NewSelected != m_DemolistSelectedIndex)
1316 {
1317 m_DemolistSelectedIndex = NewSelected;
1318 if(m_DemolistSelectedIndex >= 0)
1319 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1320 DemolistOnUpdate(Reset: false);
1321 }
1322
1323 WasListboxItemActivated = s_ListBox.WasItemActivated();
1324}
1325
1326void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView)
1327{
1328 CUIRect Contents, Header;
1329 DetailsView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &Contents);
1330 Contents.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1331 Contents.Margin(Cut: 5.0f, pOtherRect: &Contents);
1332
1333 const float FontSize = 12.0f;
1334 CDemoItem *pItem = m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex] : nullptr;
1335
1336 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1337 const char *pHeaderLabel;
1338 if(pItem == nullptr)
1339 pHeaderLabel = Localize(pStr: "No demo selected");
1340 else if(str_comp(a: pItem->m_aFilename, b: "..") == 0)
1341 pHeaderLabel = Localize(pStr: "Parent Folder");
1342 else if(pItem->m_IsLink)
1343 pHeaderLabel = Localize(pStr: "Folder Link");
1344 else if(pItem->m_IsDir)
1345 pHeaderLabel = Localize(pStr: "Folder");
1346 else if(!FetchHeader(Item&: *pItem))
1347 pHeaderLabel = Localize(pStr: "Invalid Demo");
1348 else
1349 pHeaderLabel = Localize(pStr: "Demo");
1350 Ui()->DoLabel(pRect: &Header, pText: pHeaderLabel, Size: FontSize + 2.0f, Align: TEXTALIGN_MC);
1351
1352 if(pItem == nullptr || pItem->m_IsDir)
1353 return;
1354
1355 char aBuf[256];
1356 CUIRect Left, Right;
1357
1358 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1359 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1360 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Created"), Size: FontSize, Align: TEXTALIGN_ML);
1361 if(pItem->m_Valid)
1362 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Size"), Size: FontSize, Align: TEXTALIGN_ML);
1363 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), format: TimestampFormat::SPACE);
1364 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1365 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1366 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1367
1368 if(!pItem->m_Valid)
1369 return;
1370
1371 const float DemoSize = pItem->m_Size / 1024.0f;
1372 if(DemoSize > 1024)
1373 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), DemoSize / 1024.0f);
1374 else
1375 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), DemoSize);
1376 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1377 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1378
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: Localize(pStr: "Type"), Size: FontSize, Align: TEXTALIGN_ML);
1382 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1383 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1384 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1385 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aType, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1386 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->m_Info.m_Version);
1387 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1388 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1389
1390 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1391 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1392 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Length"), Size: FontSize, Align: TEXTALIGN_ML);
1393 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Markers"), Size: FontSize, Align: TEXTALIGN_ML);
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 str_time(centisecs: (int64_t)pItem->Length() * 100, format: ETimeFormat::HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1397 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1398 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1399 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1400 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1401
1402 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1403 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Netversion"), Size: FontSize, Align: TEXTALIGN_ML);
1404 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1405 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aNetversion, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1406 Contents.HSplitTop(Cut: 16.0f, pTop: nullptr, pBottom: &Contents);
1407
1408 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1409 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map"), Size: FontSize, Align: TEXTALIGN_ML);
1410 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1411 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aMapName, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1412 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1413
1414 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1415 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map size"), Size: FontSize, Align: TEXTALIGN_ML);
1416 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1417 const float MapSize = pItem->MapSize() / 1024.0f;
1418 if(MapSize == 0.0f)
1419 str_copy(dst&: aBuf, src: Localize(pStr: "map not included", pContext: "Demo details"));
1420 else if(MapSize > 1024)
1421 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), MapSize / 1024.0f);
1422 else
1423 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), MapSize);
1424 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1425 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1426
1427 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1428 if(pItem->m_MapInfo.m_Sha256.has_value())
1429 {
1430 Ui()->DoLabel(pRect: &Left, pText: "SHA256", Size: FontSize, Align: TEXTALIGN_ML);
1431 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1432 char aSha[SHA256_MAXSTRSIZE];
1433 sha256_str(digest: pItem->m_MapInfo.m_Sha256.value(), str: aSha, max_len: sizeof(aSha));
1434 SLabelProperties Props;
1435 Props.m_MaxWidth = Left.w;
1436 Props.m_EllipsisAtEnd = true;
1437 Props.m_EnableWidthCheck = false;
1438 Ui()->DoLabel(pRect: &Left, pText: aSha, Size: FontSize - 1.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1439 }
1440 else
1441 {
1442 Ui()->DoLabel(pRect: &Left, pText: "CRC32", Size: FontSize, Align: TEXTALIGN_ML);
1443 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1444 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%08x", pItem->m_MapInfo.m_Crc);
1445 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1446 }
1447 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1448}
1449
1450void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemActivated)
1451{
1452 const auto &&SetIconMode = [&](bool Enable) {
1453 if(Enable)
1454 {
1455 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1456 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);
1457 }
1458 else
1459 {
1460 TextRender()->SetRenderFlags(0);
1461 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1462 }
1463 };
1464
1465 CUIRect ButtonBarTop, ButtonBarBottom;
1466 ButtonsView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ButtonsView);
1467 ButtonsView.HSplitMid(pTop: &ButtonBarTop, pBottom: &ButtonBarBottom, Spacing: 5.0f);
1468
1469 // quick search
1470 {
1471 CUIRect DemoSearch;
1472 ButtonBarTop.VSplitLeft(Cut: ButtonBarBottom.h * 21.0f, pLeft: &DemoSearch, pRight: &ButtonBarTop);
1473 ButtonBarTop.VSplitLeft(Cut: ButtonBarTop.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarTop);
1474 if(Ui()->DoEditBox_Search(pLineInput: &m_DemoSearchInput, pRect: &DemoSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
1475 {
1476 RefreshFilteredDemos();
1477 DemolistOnUpdate(Reset: false);
1478 }
1479 }
1480
1481 // refresh button
1482 {
1483 CUIRect RefreshButton;
1484 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 3.0f, pLeft: &RefreshButton, pRight: &ButtonBarBottom);
1485 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1486 SetIconMode(true);
1487 static CButtonContainer s_RefreshButton;
1488 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()))
1489 {
1490 SetIconMode(false);
1491 DemolistPopulate();
1492 DemolistOnUpdate(Reset: false);
1493 }
1494 SetIconMode(false);
1495 GameClient()->m_Tooltips.DoToolTip(pId: &s_RefreshButton, pNearRect: &RefreshButton, pText: Localize(pStr: "Refresh the demo list"));
1496 }
1497
1498 // fetch info checkbox
1499 {
1500 CUIRect FetchInfo;
1501 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 7.0f, pLeft: &FetchInfo, pRight: &ButtonBarBottom);
1502 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1503 if(DoButton_CheckBox(pId: &g_Config.m_BrDemoFetchInfo, pText: Localize(pStr: "Fetch Info"), Checked: g_Config.m_BrDemoFetchInfo, pRect: &FetchInfo))
1504 {
1505 g_Config.m_BrDemoFetchInfo ^= 1;
1506 if(g_Config.m_BrDemoFetchInfo)
1507 FetchAllHeaders();
1508 }
1509 }
1510
1511 // demos directory button
1512 if(m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
1513 {
1514 CUIRect DemosDirectoryButton;
1515 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 10.0f, pLeft: &DemosDirectoryButton, pRight: &ButtonBarBottom);
1516 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1517 static CButtonContainer s_DemosDirectoryButton;
1518 if(DoButton_Menu(pButtonContainer: &s_DemosDirectoryButton, pText: Localize(pStr: "Demos directory"), Checked: 0, pRect: &DemosDirectoryButton))
1519 {
1520 char aBuf[IO_MAX_PATH_LENGTH];
1521 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));
1522 Client()->ViewFile(pFilename: aBuf);
1523 }
1524 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemosDirectoryButton, pNearRect: &DemosDirectoryButton, pText: Localize(pStr: "Open the directory that contains the demo files"));
1525 }
1526
1527 // play/open button
1528 if(m_DemolistSelectedIndex >= 0)
1529 {
1530 CUIRect PlayButton;
1531 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &PlayButton);
1532 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarBottom, pRight: nullptr);
1533 SetIconMode(true);
1534 static CButtonContainer s_PlayButton;
1535 if(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 || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || (Input()->KeyPress(Key: KEY_P) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1536 {
1537 SetIconMode(false);
1538 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
1539 {
1540 m_DemoSearchInput.Clear();
1541 const bool ParentFolder = str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") == 0;
1542 if(ParentFolder) // parent folder
1543 {
1544 str_copy(dst&: m_aCurrentDemoSelectionName, src: fs_filename(path: m_aCurrentDemoFolder));
1545 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1546 if(fs_parent_dir(path: m_aCurrentDemoFolder))
1547 {
1548 m_aCurrentDemoFolder[0] = '\0';
1549 if(m_DemolistStorageType == IStorage::TYPE_ALL)
1550 {
1551 m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item
1552 }
1553 else
1554 {
1555 Storage()->GetCompletePath(Type: m_DemolistStorageType, pDir: "demos", pBuffer: m_aCurrentDemoSelectionName, BufferSize: sizeof(m_aCurrentDemoSelectionName));
1556 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1557 }
1558 }
1559 }
1560 else // sub folder
1561 {
1562 if(m_aCurrentDemoFolder[0] != '\0')
1563 str_append(dst&: m_aCurrentDemoFolder, src: "/");
1564 else
1565 m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
1566 str_append(dst&: m_aCurrentDemoFolder, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1567 }
1568 DemolistPopulate();
1569 DemolistOnUpdate(Reset: !ParentFolder);
1570 }
1571 else // file
1572 {
1573 if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
1574 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);
1575 else
1576 CMenus::PopupConfirmPlayDemo();
1577 return;
1578 }
1579 }
1580 SetIconMode(false);
1581 const char *pPlayTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Open the selected folder") : Localize(pStr: "Play the selected demo");
1582 GameClient()->m_Tooltips.DoToolTip(pId: &s_PlayButton, pNearRect: &PlayButton, pText: pPlayTooltip);
1583
1584 if(m_aCurrentDemoFolder[0] != '\0')
1585 {
1586 if(str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
1587 {
1588 // rename button
1589 CUIRect RenameButton;
1590 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &RenameButton);
1591 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1592 SetIconMode(true);
1593 static CButtonContainer s_RenameButton;
1594 if(DoButton_Menu(pButtonContainer: &s_RenameButton, pText: FontIcon::PENCIL, Checked: 0, pRect: &RenameButton))
1595 {
1596 SetIconMode(false);
1597 m_Popup = POPUP_RENAME_DEMO;
1598 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1599 {
1600 m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1601 }
1602 else
1603 {
1604 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1605 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1606 m_DemoRenameInput.Set(aNameWithoutExt);
1607 }
1608 Ui()->SetActiveItem(&m_DemoRenameInput);
1609 return;
1610 }
1611 const char *pRenameTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Rename folder") : Localize(pStr: "Rename demo");
1612 GameClient()->m_Tooltips.DoToolTip(pId: &s_RenameButton, pNearRect: &RenameButton, pText: pRenameTooltip);
1613
1614 // delete button
1615 static CButtonContainer s_DeleteButton;
1616 CUIRect DeleteButton;
1617 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &DeleteButton);
1618 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1619 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()))
1620 {
1621 SetIconMode(false);
1622 char aBuf[128 + IO_MAX_PATH_LENGTH];
1623 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);
1624 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);
1625 return;
1626 }
1627 const char *pDeleteTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Delete folder") : Localize(pStr: "Delete demo");
1628 GameClient()->m_Tooltips.DoToolTip(pId: &s_DeleteButton, pNearRect: &DeleteButton, pText: pDeleteTooltip);
1629 SetIconMode(false);
1630 }
1631
1632#if defined(CONF_VIDEORECORDER)
1633 // render demo button
1634 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1635 {
1636 CUIRect RenderButton;
1637 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarTop, pRight: &RenderButton);
1638 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarTop, pRight: nullptr);
1639 SetIconMode(true);
1640 static CButtonContainer s_RenderButton;
1641 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()))
1642 {
1643 SetIconMode(false);
1644 m_Popup = POPUP_RENDER_DEMO;
1645 m_StartPaused = false;
1646 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1647 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1648 m_DemoRenderInput.Set(aNameWithoutExt);
1649 Ui()->SetActiveItem(&m_DemoRenderInput);
1650 return;
1651 }
1652 SetIconMode(false);
1653 GameClient()->m_Tooltips.DoToolTip(pId: &s_RenderButton, pNearRect: &RenderButton, pText: Localize(pStr: "Render demo"));
1654 }
1655#endif
1656 }
1657 }
1658}
1659
1660void CMenus::PopupConfirmPlayDemo()
1661{
1662 char aBuf[IO_MAX_PATH_LENGTH];
1663 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1664 const char *pError = Client()->DemoPlayer_Play(pFilename: aBuf, StorageType: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType);
1665 m_LastPauseChange = -1.0f;
1666 m_LastSpeedChange = -1.0f;
1667 if(pError)
1668 {
1669 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
1670 }
1671 else
1672 {
1673 Ui()->SetActiveItem(nullptr);
1674 return;
1675 }
1676}
1677
1678void CMenus::PopupConfirmDeleteDemo()
1679{
1680 char aBuf[IO_MAX_PATH_LENGTH];
1681 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1682 if(Storage()->RemoveFile(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1683 {
1684 DemolistPopulate();
1685 DemolistOnUpdate(Reset: false);
1686 }
1687 else
1688 {
1689 char aError[128 + IO_MAX_PATH_LENGTH];
1690 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the demo '%s'"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1691 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1692 }
1693}
1694
1695void CMenus::PopupConfirmDeleteFolder()
1696{
1697 char aBuf[IO_MAX_PATH_LENGTH];
1698 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1699 if(Storage()->RemoveFolder(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1700 {
1701 DemolistPopulate();
1702 DemolistOnUpdate(Reset: false);
1703 }
1704 else
1705 {
1706 char aError[128 + IO_MAX_PATH_LENGTH];
1707 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);
1708 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1709 }
1710}
1711
1712void CMenus::ConchainDemoPlay(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1713{
1714 CMenus *pThis = static_cast<CMenus *>(pUserData);
1715 pThis->m_LastPauseChange = pThis->Client()->GlobalTime();
1716 pfnCallback(pResult, pCallbackUserData);
1717}
1718
1719void CMenus::ConchainDemoSpeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1720{
1721 CMenus *pThis = static_cast<CMenus *>(pUserData);
1722 if(pResult->NumArguments() == 1)
1723 {
1724 pThis->m_LastSpeedChange = pThis->Client()->GlobalTime();
1725 }
1726 pfnCallback(pResult, pCallbackUserData);
1727}
1728