1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 2020 Warzone 2100 Project
4
5 Warzone 2100 is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 Warzone 2100 is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Warzone 2100; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19 /*
20 * musicmanager.cpp
21 *
22 * This is the Music Manager screen.
23 */
24
25 #include "musicmanager.h"
26
27 #include "lib/ivis_opengl/pieblitfunc.h"
28 #include "intdisplay.h"
29 #include "hci.h"
30 #include "multiint.h"
31 #include "frontend.h"
32 #include "frend.h"
33 #include "ingameop.h"
34 #include "keymap.h"
35 #include "keybind.h"
36
37 #include "lib/sound/playlist.h"
38 #include "lib/widget/button.h"
39 #include "lib/widget/label.h"
40 #include "lib/widget/scrollablelist.h"
41 #include <chrono>
42 #include <memory>
43
44
45 #define W_TRACK_ROW_PADDING 5
46 #define W_TRACK_COL_PADDING 10
47 #define W_TRACK_CHECKBOX_SIZE 12
48
49 #define W_TRACK_COL_TITLE_X W_TRACK_COL_PADDING
50 #define W_TRACK_COL_TITLE_W 180
51 #define W_TRACK_COL_ALBUM_X (W_TRACK_COL_TITLE_X + W_TRACK_COL_TITLE_W + W_TRACK_COL_PADDING)
52 #define W_TRACK_COL_ALBUM_W 130
53 #define W_TRACK_CHECKBOX_STARTINGPOS (W_TRACK_COL_ALBUM_X + W_TRACK_COL_ALBUM_W + W_TRACK_COL_PADDING + W_TRACK_COL_PADDING)
54
55 #define W_TRACK_HEADER_Y 20
56 #define W_TRACK_HEADER_HEIGHT (20 + (W_TRACK_ROW_PADDING * 2))
57 #define W_TRACK_HEADER_COL_IMAGE_SIZE 16
58
59 #define TL_W FRONTEND_BOTFORMW
60 #define TL_H 400
61 #define TL_X FRONTEND_BOTFORMX
62 #define TL_Y (W_TRACK_HEADER_Y + W_TRACK_HEADER_HEIGHT)
63 #define TL_SX FRONTEND_SIDEX
64
65 #define TL_ENTRYW (FRONTEND_BOTFORMW - 80)
66 #define TL_ENTRYH (25)
67
68 #define TL_PREVIEWBOX_Y_SPACING 45
69 #define TL_PREVIEWBOX_H 80
70
GetTrackListHeight()71 static int GetTrackListHeight()
72 {
73 if (pie_GetVideoBufferHeight() > (BASE_COORDS_Y + TL_PREVIEWBOX_Y_SPACING + 20 + TL_PREVIEWBOX_H))
74 {
75 return TL_H;
76 }
77 return pie_GetVideoBufferHeight() - TL_Y - 20 - TL_PREVIEWBOX_Y_SPACING - TL_PREVIEWBOX_H;
78 }
79
GetNumVisibleTracks()80 static int GetNumVisibleTracks()
81 {
82 int maxTracks = static_cast<int>(std::floor(float(GetTrackListHeight() - TL_Y - W_TRACK_HEADER_Y) / float(TL_ENTRYH)));
83 return maxTracks;
84 }
85
GetDetailsBoxStartYPos()86 static int GetDetailsBoxStartYPos()
87 {
88 return GetTrackListHeight() + TL_PREVIEWBOX_Y_SPACING;
89 }
90
GetTotalTrackAndDetailsBoxHeight()91 static int GetTotalTrackAndDetailsBoxHeight()
92 {
93 return GetTrackListHeight() + TL_PREVIEWBOX_Y_SPACING + TL_PREVIEWBOX_H;
94 }
95
GetTrackListStartXPos(int ingame)96 static int GetTrackListStartXPos(int ingame)
97 {
98 if (!ingame)
99 {
100 return TL_X;
101 }
102 return TL_X - 10;
103 }
104
105 // MARK: - MusicManager_CDAudioEventSink declaration
106 class MusicManager_CDAudioEventSink : public CDAudioEventSink
107 {
108 public:
~MusicManager_CDAudioEventSink()109 virtual ~MusicManager_CDAudioEventSink() override {};
110 virtual void startedPlayingTrack(const std::shared_ptr<const WZ_TRACK>& track) override;
111 virtual void trackEnded(const std::shared_ptr<const WZ_TRACK>& track) override;
112 virtual void musicStopped() override;
113 virtual void musicPaused(const std::shared_ptr<const WZ_TRACK>& track) override;
114 virtual void musicResumed(const std::shared_ptr<const WZ_TRACK>& track) override;
unregisterEventSink() const115 virtual bool unregisterEventSink() const override { return shouldUnregisterEventSink; }
116 public:
setUnregisterEventSink()117 void setUnregisterEventSink()
118 {
119 shouldUnregisterEventSink = true;
120 }
121 private:
122 bool shouldUnregisterEventSink = false;
123 };
124
125 // MARK: - Globals
126
127 struct TrackRowCache; // forward-declare
128 class W_TrackRow; // foward-declare
129 static std::unordered_map<W_TrackRow*, std::shared_ptr<TrackRowCache>> trackRowsCache;
130 static std::vector<std::shared_ptr<const WZ_TRACK>> trackList;
131 static std::shared_ptr<const WZ_TRACK> selectedTrack;
132 static std::shared_ptr<MusicManager_CDAudioEventSink> musicManagerAudioEventSink;
133
134 // now-playing widgets
135 static std::shared_ptr<W_LABEL> psNowPlaying = nullptr;
136 static std::shared_ptr<W_LABEL> psSelectedTrackName = nullptr;
137 static std::shared_ptr<W_LABEL> psSelectedTrackAuthorName = nullptr;
138 static std::shared_ptr<W_LABEL> psSelectedTrackAlbumName = nullptr;
139 static std::shared_ptr<W_LABEL> psSelectedTrackAlbumDate = nullptr;
140 static std::shared_ptr<W_LABEL> psSelectedTrackAlbumDescription = nullptr;
141
142 // MARK: - W_MusicModeCheckboxButton
143
144 struct W_MusicModeCheckboxButton : public W_BUTTON
145 {
146 public:
W_MusicModeCheckboxButtonW_MusicModeCheckboxButton147 W_MusicModeCheckboxButton(MusicGameMode mode, bool isChecked)
148 : W_BUTTON()
149 , mode(mode)
150 , isChecked(isChecked)
151 {
152 addOnClickHandler([](W_BUTTON& button) {
153 W_MusicModeCheckboxButton& self = dynamic_cast<W_MusicModeCheckboxButton&>(button);
154 if (!self.isEnabled()) { return; }
155 self.isChecked = !self.isChecked;
156 });
157 }
158 void display(int xOffset, int yOffset) override;
159 void highlight(W_CONTEXT *psContext) override;
160 void highlightLost() override;
161
getIsCheckedW_MusicModeCheckboxButton162 bool getIsChecked() const { return isChecked; }
163
setCheckboxSizeW_MusicModeCheckboxButton164 void setCheckboxSize(int size)
165 {
166 cbSize = size;
167 }
checkboxSizeW_MusicModeCheckboxButton168 int checkboxSize() const { return cbSize; }
169
getMusicModeW_MusicModeCheckboxButton170 MusicGameMode getMusicMode() { return mode; }
171 private:
isEnabledW_MusicModeCheckboxButton172 bool isEnabled() { return (getState() & WBUT_DISABLE) == 0; }
173 private:
174 MusicGameMode mode;
175 bool isChecked = false;
176 int cbSize = 0;
177 };
178
display(int xOffset,int yOffset)179 void W_MusicModeCheckboxButton::display(int xOffset, int yOffset)
180 {
181 int x0 = xOffset + x();
182 int y0 = yOffset + y();
183
184 bool down = (getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
185 bool isDisabled = (getState() & WBUT_DISABLE) != 0;
186
187 // calculate checkbox dimensions
188 Vector2i checkboxOffset{0, (height() - cbSize) / 2}; // left-align, center vertically
189 Vector2i checkboxPos{x0 + checkboxOffset.x, y0 + checkboxOffset.y};
190
191 // draw checkbox border
192 PIELIGHT notifyBoxAddColor = WZCOL_NOTIFICATION_BOX;
193 notifyBoxAddColor.byte.a = uint8_t(float(notifyBoxAddColor.byte.a) * ((!isDisabled) ? 0.7f : 0.2f));
194 pie_UniTransBoxFill(checkboxPos.x, checkboxPos.y, checkboxPos.x + cbSize, checkboxPos.y + cbSize, notifyBoxAddColor);
195 PIELIGHT checkBoxOutsideColor = WZCOL_TEXT_MEDIUM;
196 if (isDisabled)
197 {
198 checkBoxOutsideColor.byte.a = 60;
199 }
200 iV_Box2(checkboxPos.x, checkboxPos.y, checkboxPos.x + cbSize, checkboxPos.y + cbSize, checkBoxOutsideColor, checkBoxOutsideColor);
201
202 if (down || isChecked)
203 {
204 // draw checkbox "checked" inside
205 #define CB_INNER_INSET 2
206 PIELIGHT checkBoxInsideColor = WZCOL_TEXT_MEDIUM;
207 checkBoxInsideColor.byte.a = (!isDisabled) ? 200 : 60;
208 pie_UniTransBoxFill(checkboxPos.x + CB_INNER_INSET, checkboxPos.y + CB_INNER_INSET, checkboxPos.x + cbSize - (CB_INNER_INSET), checkboxPos.y + cbSize - (CB_INNER_INSET), checkBoxInsideColor);
209 }
210 }
211
highlight(W_CONTEXT * psContext)212 void W_MusicModeCheckboxButton::highlight(W_CONTEXT *psContext)
213 {
214 if (!isEnabled()) return;
215 W_BUTTON::highlight(psContext);
216 }
217
highlightLost()218 void W_MusicModeCheckboxButton::highlightLost()
219 {
220 if (!isEnabled()) return;
221 W_BUTTON::highlightLost();
222 }
223
224 // MARK: - W_TrackRow
225
226 struct TrackRowCache {
227 WzText wzText_Title;
228 WzText wzText_Album;
229 UDWORD lastUsedFrameNumber;
230 };
231
232 class W_TrackRow : public W_BUTTON
233 {
234 protected:
W_TrackRow(W_BUTINIT const * init,std::shared_ptr<const WZ_TRACK> track)235 W_TrackRow(W_BUTINIT const *init, std::shared_ptr<const WZ_TRACK> track): W_BUTTON(init), track(std::move(track)) {}
236 void initialize(bool ingame);
237
238 public:
make(W_BUTINIT const * init,std::shared_ptr<const WZ_TRACK> const & track,bool ingame)239 static std::shared_ptr<W_TrackRow> make(W_BUTINIT const *init, std::shared_ptr<const WZ_TRACK> const &track, bool ingame)
240 {
241 class make_shared_enabler: public W_TrackRow
242 {
243 public:
244 make_shared_enabler(W_BUTINIT const *init, std::shared_ptr<const WZ_TRACK> const &track): W_TrackRow(init, track) {}
245 };
246 auto widget = std::make_shared<make_shared_enabler>(init, track);
247 widget->initialize(ingame);
248 return widget;
249 }
250
251 void display(int xOffset, int yOffset) override;
252
getTrack() const253 std::shared_ptr<const WZ_TRACK> getTrack() const {
254 return std::shared_ptr<const WZ_TRACK>(track);
255 };
256
257 protected:
258 void geometryChanged() override;
259
260 private:
261 std::shared_ptr<const WZ_TRACK> track;
262 std::string album_name;
263 std::vector<std::shared_ptr<W_MusicModeCheckboxButton>> musicModeCheckboxes;
264 MusicGameMode musicMode = MusicGameMode::MENUS;
265 };
266
initialize(bool ingame)267 void W_TrackRow::initialize(bool ingame)
268 {
269 auto album = track->album.lock();
270 album_name = album->title;
271
272 // add music mode checkboxes
273 musicMode = MusicGameMode::MENUS;
274 if (ingame)
275 {
276 musicMode = PlayList_GetCurrentMusicMode();
277 }
278
279 for (int musicModeIdx = 0; musicModeIdx < NUM_MUSICGAMEMODES; musicModeIdx++)
280 {
281 auto pCheckBox = std::make_shared<W_MusicModeCheckboxButton>(static_cast<MusicGameMode>(musicModeIdx), PlayList_IsTrackEnabledForMusicMode(track, static_cast<MusicGameMode>(musicModeIdx)));
282 attach(pCheckBox);
283 auto captureTrack = track;
284 pCheckBox->addOnClickHandler([captureTrack](W_BUTTON& button) {
285 W_MusicModeCheckboxButton& self = dynamic_cast<W_MusicModeCheckboxButton&>(button);
286 PlayList_SetTrackMusicMode(captureTrack, self.getMusicMode(), self.getIsChecked());
287 });
288 pCheckBox->setGeometry(W_TRACK_CHECKBOX_STARTINGPOS + ((W_TRACK_CHECKBOX_SIZE + W_TRACK_COL_PADDING) * musicModeIdx), W_TRACK_ROW_PADDING, W_TRACK_CHECKBOX_SIZE, W_TRACK_CHECKBOX_SIZE);
289 pCheckBox->setCheckboxSize(W_TRACK_CHECKBOX_SIZE);
290 if (musicMode != MusicGameMode::MENUS)
291 {
292 if (musicModeIdx != static_cast<int>(musicMode))
293 {
294 pCheckBox->setState(WBUT_DISABLE);
295 }
296 }
297
298 musicModeCheckboxes.push_back(pCheckBox);
299 }
300 }
301
geometryChanged()302 void W_TrackRow::geometryChanged()
303 {
304 for (size_t i = 0; i < musicModeCheckboxes.size(); i++)
305 {
306 auto pCB = musicModeCheckboxes[i];
307 pCB->setGeometry(W_TRACK_CHECKBOX_STARTINGPOS + ((W_TRACK_CHECKBOX_SIZE + W_TRACK_COL_PADDING) * i), std::max(W_TRACK_ROW_PADDING, (height() - W_TRACK_CHECKBOX_SIZE) / 2), W_TRACK_CHECKBOX_SIZE, W_TRACK_CHECKBOX_SIZE);
308 }
309 }
310
truncateTextToMaxWidth(std::string str,iV_fonts fontID,int maxWidth)311 static std::string truncateTextToMaxWidth(std::string str, iV_fonts fontID, int maxWidth)
312 {
313 if ((int)iV_GetTextWidth(str.c_str(), fontID) > maxWidth)
314 {
315 while (!str.empty() && (int)iV_GetTextWidth((str + "...").c_str(), fontID) > maxWidth)
316 {
317 str.resize(str.size() - 1); // Clip name.
318 }
319 str += "...";
320 }
321 return str;
322 }
323
display(int xOffset,int yOffset)324 void W_TrackRow::display(int xOffset, int yOffset)
325 {
326 bool isSelectedTrack = (track == selectedTrack);
327
328 // get track cache
329 std::shared_ptr<TrackRowCache> pCache = nullptr;
330 auto it = trackRowsCache.find(this);
331 if (it != trackRowsCache.end())
332 {
333 pCache = it->second;
334 }
335 if (!pCache)
336 {
337 pCache = std::make_shared<TrackRowCache>();
338 trackRowsCache[this] = pCache;
339
340 // calculate max displayable length for title and album
341 std::string title_truncated = truncateTextToMaxWidth(track->title, font_regular, W_TRACK_COL_TITLE_W);
342 std::string album_truncated = truncateTextToMaxWidth(album_name, font_regular, W_TRACK_COL_ALBUM_W);
343 pCache->wzText_Title.setText(title_truncated, font_regular);
344 pCache->wzText_Album.setText(album_truncated, font_regular);
345 }
346 pCache->lastUsedFrameNumber = frameGetFrameNumber();
347
348 int x0 = x() + xOffset;
349 int y0 = y() + yOffset;
350 int x1 = x0 + width();
351 int y1 = y0 + height();
352
353 if (isSelectedTrack)
354 {
355 // draw selected background
356 pie_UniTransBoxFill(x0, y0, x1, y1, WZCOL_KEYMAP_ACTIVE);
357 }
358
359 PIELIGHT textColor = WZCOL_TEXT_BRIGHT;
360 if ((musicMode != MusicGameMode::MENUS) && !PlayList_IsTrackEnabledForMusicMode(track, musicMode))
361 {
362 textColor.byte.a = uint8_t(float(textColor.byte.a) * 0.6f);
363 }
364
365 // draw track title
366 Vector2i textBoundingBoxOffset(0, 0);
367 textBoundingBoxOffset.y = yOffset + y() + (height() - pCache->wzText_Title.lineSize()) / 2;
368 float fy = float(textBoundingBoxOffset.y) /*+ float(W_TRACK_ROW_PADDING)*/ - float(pCache->wzText_Title.aboveBase());
369 pCache->wzText_Title.render(W_TRACK_COL_TITLE_X + x0, static_cast<int>(fy), textColor);
370
371 // draw album name
372 pCache->wzText_Album.render(W_TRACK_COL_ALBUM_X + x0, static_cast<int>(fy), textColor);
373
374 // Music mode checkboxes are child widgets
375 }
376
377 // MARK: - "Now Playing" Details Block
378
loadImageToTexture(const std::string & imagePath)379 static gfx_api::texture* loadImageToTexture(const std::string& imagePath)
380 {
381 if (imagePath.empty())
382 {
383 return nullptr;
384 }
385
386 iV_Image ivImage;
387 const std::string& filename = imagePath;
388 const char *extension = strrchr(filename.c_str(), '.'); // determine the filetype
389
390 if (!extension || strcmp(extension, ".png") != 0)
391 {
392 debug(LOG_ERROR, "Bad image filename: %s", filename.c_str());
393 return nullptr;
394 }
395 if (!iV_loadImage_PNG(filename.c_str(), &ivImage))
396 {
397 return nullptr;
398 }
399 auto pixel_format = iV_getPixelFormat(&ivImage);
400 size_t mip_count = floor(log(std::max(ivImage.width, ivImage.height))) + 1;
401 gfx_api::texture* pAlbumCoverTexture = gfx_api::context::get().create_texture(mip_count, ivImage.width, ivImage.height, pixel_format);
402 pAlbumCoverTexture->upload_and_generate_mipmaps(0u, 0u, ivImage.width, ivImage.height, pixel_format, ivImage.bmp);
403 iV_unloadImage(&ivImage);
404 return pAlbumCoverTexture;
405 }
406
407 class TrackDetailsForm : public IntFormAnimated
408 {
409 public:
TrackDetailsForm()410 TrackDetailsForm()
411 : IntFormAnimated(true)
412 { }
413 virtual void display(int xOffset, int yOffset) override;
~TrackDetailsForm()414 virtual ~TrackDetailsForm() override
415 {
416 if (pAlbumCoverTexture)
417 {
418 delete pAlbumCoverTexture;
419 pAlbumCoverTexture = nullptr;
420 }
421 }
422
loadImage(const std::string & imagePath)423 bool loadImage(const std::string& imagePath)
424 {
425 if (imagePath.empty())
426 {
427 if (pAlbumCoverTexture)
428 {
429 delete pAlbumCoverTexture;
430 pAlbumCoverTexture = nullptr;
431 }
432 album_cover_path.clear();
433 return true;
434 }
435
436 if (imagePath == album_cover_path)
437 {
438 // already loaded
439 return true;
440 }
441
442 if (pAlbumCoverTexture)
443 {
444 delete pAlbumCoverTexture;
445 pAlbumCoverTexture = nullptr;
446 }
447 pAlbumCoverTexture = loadImageToTexture(imagePath);
448 if (!pAlbumCoverTexture)
449 {
450 album_cover_path.clear();
451 return false;
452 }
453 album_cover_path = imagePath;
454 return true;
455 }
456
457 private:
458 gfx_api::texture* pAlbumCoverTexture = nullptr;
459 std::string album_cover_path;
460 };
461
462 #define WZ_TRACKDETAILS_IMAGE_X (20 + 210 + 10)
463 #define WZ_TRACKDETAILS_IMAGE_Y 10
464 #define WZ_TRACKDETAILS_IMAGE_SIZE 60
465
display(int xOffset,int yOffset)466 void TrackDetailsForm::display(int xOffset, int yOffset)
467 {
468 IntFormAnimated::display(xOffset, yOffset);
469
470 if (disableChildren) { return; }
471
472 // now draw the album cover, if present
473 int imageLeft = x() + xOffset + WZ_TRACKDETAILS_IMAGE_X;
474 int imageTop = y() + yOffset + WZ_TRACKDETAILS_IMAGE_Y;
475
476 if (pAlbumCoverTexture)
477 {
478 iV_DrawImageAnisotropic(*pAlbumCoverTexture, Vector2i(imageLeft, imageTop), Vector2f(0,0), Vector2f(WZ_TRACKDETAILS_IMAGE_SIZE, WZ_TRACKDETAILS_IMAGE_SIZE), 0.f, WZCOL_WHITE);
479 }
480 }
481
UpdateTrackDetailsBox(TrackDetailsForm * pTrackDetailsBox)482 static void UpdateTrackDetailsBox(TrackDetailsForm *pTrackDetailsBox)
483 {
484 ASSERT_OR_RETURN(, pTrackDetailsBox, "pTrackDetailsBox is null");
485
486 // Add "Now Playing" label
487 if (!psNowPlaying)
488 {
489 pTrackDetailsBox->attach(psNowPlaying = std::make_shared<W_LABEL>());
490 }
491 psNowPlaying->setGeometry(20, 12, 210, 20);
492 psNowPlaying->setFont(font_regular, WZCOL_TEXT_MEDIUM);
493 psNowPlaying->setString((selectedTrack) ? (WzString::fromUtf8(_("NOW PLAYING")) + ":") : "");
494 psNowPlaying->setTextAlignment(WLAB_ALIGNTOPLEFT);
495 // set a custom hit-testing function that ignores all mouse input / clicks
496 psNowPlaying->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
497
498 // Add Selected Track name
499 if (!psSelectedTrackName)
500 {
501 pTrackDetailsBox->attach(psSelectedTrackName = std::make_shared<W_LABEL>());
502 }
503 psSelectedTrackName->setGeometry(20, 12 + 20, 210, 20);
504 psSelectedTrackName->setFont(font_regular_bold, WZCOL_TEXT_BRIGHT);
505 psSelectedTrackName->setString((selectedTrack) ? WzString::fromUtf8(selectedTrack->title) : "");
506 psSelectedTrackName->setTextAlignment(WLAB_ALIGNTOPLEFT);
507 // set a custom hit-testing function that ignores all mouse input / clicks
508 psSelectedTrackName->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
509
510 // Add Selected Track author details
511 if (!psSelectedTrackAuthorName)
512 {
513 pTrackDetailsBox->attach(psSelectedTrackAuthorName = std::make_shared<W_LABEL>());
514 }
515 psSelectedTrackAuthorName->setGeometry(20, 12 + 20 + 17, 210, 20);
516 psSelectedTrackAuthorName->setFont(font_regular, WZCOL_TEXT_BRIGHT);
517 psSelectedTrackAuthorName->setString((selectedTrack) ? WzString::fromUtf8(selectedTrack->author) : "");
518 psSelectedTrackAuthorName->setTextAlignment(WLAB_ALIGNTOPLEFT);
519 // set a custom hit-testing function that ignores all mouse input / clicks
520 psSelectedTrackAuthorName->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
521
522 // album info xPosStart
523 int albumInfoXPosStart = psNowPlaying->x() + psNowPlaying->width() + 10 + WZ_TRACKDETAILS_IMAGE_SIZE + 10;
524 int maxWidthOfAlbumLabel = TL_W - albumInfoXPosStart - 20;
525 std::shared_ptr<const WZ_ALBUM> pAlbum = (selectedTrack) ? selectedTrack->album.lock() : nullptr;
526
527 if (!psSelectedTrackAlbumName)
528 {
529 pTrackDetailsBox->attach(psSelectedTrackAlbumName = std::make_shared<W_LABEL>());
530 }
531 psSelectedTrackAlbumName->setGeometry(albumInfoXPosStart, 12, maxWidthOfAlbumLabel, 20);
532 psSelectedTrackAlbumName->setFont(font_small, WZCOL_TEXT_BRIGHT);
533 psSelectedTrackAlbumName->setString((pAlbum) ? (WzString::fromUtf8(_("Album")) + ": " + WzString::fromUtf8(pAlbum->title)) : "");
534 psSelectedTrackAlbumName->setTextAlignment(WLAB_ALIGNTOPLEFT);
535 // set a custom hit-testing function that ignores all mouse input / clicks
536 psSelectedTrackAlbumName->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
537
538 if (!psSelectedTrackAlbumDate)
539 {
540 pTrackDetailsBox->attach(psSelectedTrackAlbumDate = std::make_shared<W_LABEL>());
541 }
542 psSelectedTrackAlbumDate->setGeometry(albumInfoXPosStart, 12 + 13, maxWidthOfAlbumLabel, 20);
543 psSelectedTrackAlbumDate->setFont(font_small, WZCOL_TEXT_BRIGHT);
544 psSelectedTrackAlbumDate->setString((pAlbum) ? (WzString::fromUtf8(_("Date")) + ": " + WzString::fromUtf8(pAlbum->date)) : "");
545 psSelectedTrackAlbumDate->setTextAlignment(WLAB_ALIGNTOPLEFT);
546 // set a custom hit-testing function that ignores all mouse input / clicks
547 psSelectedTrackAlbumDate->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
548
549 if (!psSelectedTrackAlbumDescription)
550 {
551 pTrackDetailsBox->attach(psSelectedTrackAlbumDescription = std::make_shared<W_LABEL>());
552 }
553 psSelectedTrackAlbumDescription->setGeometry(albumInfoXPosStart, 12 + 13 + 15, maxWidthOfAlbumLabel, 20);
554 psSelectedTrackAlbumDescription->setFont(font_small, WZCOL_TEXT_BRIGHT);
555 int heightOfTitleLabel = psSelectedTrackAlbumDescription->setFormattedString((pAlbum) ? WzString::fromUtf8(pAlbum->description) : "", maxWidthOfAlbumLabel, font_small);
556 psSelectedTrackAlbumDescription->setGeometry(albumInfoXPosStart, 12 + 13 + 15, maxWidthOfAlbumLabel, heightOfTitleLabel);
557 psSelectedTrackAlbumDescription->setTextAlignment(WLAB_ALIGNTOPLEFT);
558 // set a custom hit-testing function that ignores all mouse input / clicks
559 psSelectedTrackAlbumDescription->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
560
561 pTrackDetailsBox->loadImage((pAlbum) ? pAlbum->album_cover_filename : "");
562 }
563
addTrackDetailsBox(WIDGET * parent,bool ingame)564 static void addTrackDetailsBox(WIDGET *parent, bool ingame)
565 {
566 if (widgGetFromID(psWScreen, MULTIOP_CONSOLEBOX))
567 {
568 return;
569 }
570
571 auto pTrackDetailsBox = std::make_shared<TrackDetailsForm>();
572 parent->attach(pTrackDetailsBox);
573 pTrackDetailsBox->id = MULTIOP_CONSOLEBOX;
574 if (!ingame)
575 {
576 pTrackDetailsBox->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
577 psWidget->setGeometry(TL_X, GetDetailsBoxStartYPos(), TL_W, TL_PREVIEWBOX_H);
578 }));
579 }
580 else
581 {
582 pTrackDetailsBox->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
583 psWidget->setGeometry(((300-(TL_W/2))+D_W), ((240-(GetTotalTrackAndDetailsBoxHeight()/2))+D_H) + GetDetailsBoxStartYPos(), TL_W, TL_PREVIEWBOX_H);
584 }));
585 }
586
587 UpdateTrackDetailsBox(pTrackDetailsBox.get());
588 }
589
590 // MARK: - Track List
591
592 class MusicListHeader : public W_FORM
593 {
594 public:
595 MusicListHeader();
596 virtual void display(int xOffset, int yOffset);
597 };
598
MusicListHeader()599 MusicListHeader::MusicListHeader()
600 : W_FORM()
601 { }
602
display(int xOffset,int yOffset)603 void MusicListHeader::display(int xOffset, int yOffset)
604 {
605 int x0 = x() + xOffset;
606 int y0 = y() + yOffset;
607 int x1 = x0 + width();
608 int y1 = y0 + height() - 1;
609 iV_TransBoxFill(x0, y0, x1, y1);
610 iV_Line(x0, y1, x1, y1, WZCOL_MENU_SEPARATOR);
611
612 // column lines
613 iV_Line(x0 + W_TRACK_COL_TITLE_X + W_TRACK_COL_TITLE_W, y0 + 5, x0 + W_TRACK_COL_TITLE_X + W_TRACK_COL_TITLE_W, y1 - 5, WZCOL_MENU_SEPARATOR);
614 iV_Line(x0 + W_TRACK_COL_ALBUM_X + W_TRACK_COL_ALBUM_W, y0 + 5, x0 + W_TRACK_COL_ALBUM_X + W_TRACK_COL_ALBUM_W, y1 - 5, WZCOL_MENU_SEPARATOR);
615 }
616
617 class W_MusicListHeaderColImage : public W_BUTTON
618 {
619 public:
W_MusicListHeaderColImage()620 W_MusicListHeaderColImage()
621 : W_BUTTON()
622 {
623 AudioCallback = nullptr;
624 }
625
loadImage(const std::string & imagePath)626 bool loadImage(const std::string& imagePath)
627 {
628 if (pAlbumCoverTexture)
629 {
630 delete pAlbumCoverTexture;
631 pAlbumCoverTexture = nullptr;
632 }
633 pAlbumCoverTexture = loadImageToTexture(imagePath);
634 return pAlbumCoverTexture != nullptr;
635 }
636
display(int xOffset,int yOffset)637 void display(int xOffset, int yOffset) override
638 {
639 if (pAlbumCoverTexture)
640 {
641 int imageLeft = x() + xOffset;
642 int imageTop = y() + yOffset;
643
644 iV_DrawImageAnisotropic(*pAlbumCoverTexture, Vector2i(imageLeft, imageTop), Vector2f(0,0), Vector2f(W_TRACK_HEADER_COL_IMAGE_SIZE, W_TRACK_HEADER_COL_IMAGE_SIZE), 0.f, WZCOL_WHITE);
645 }
646 }
647 private:
648 gfx_api::texture* pAlbumCoverTexture = nullptr;
649 };
650
651 static const std::string music_mode_col_header_images[] = {
652 "images/frontend/image_music_campaign.png",
653 "images/frontend/image_music_challenges.png",
654 "images/frontend/image_music_skirmish.png",
655 "images/frontend/image_music_multiplayer.png"
656 };
657
addTrackList(WIDGET * parent,bool ingame)658 static void addTrackList(WIDGET *parent, bool ingame)
659 {
660 auto pHeader = std::make_shared<MusicListHeader>();
661 parent->attach(pHeader);
662 pHeader->setGeometry(GetTrackListStartXPos(ingame), W_TRACK_HEADER_Y, TL_ENTRYW, W_TRACK_HEADER_HEIGHT);
663
664 const int headerColY = W_TRACK_ROW_PADDING + (W_TRACK_ROW_PADDING / 2);
665
666 auto title_header = std::make_shared<W_LABEL>();
667 pHeader->attach(title_header);
668 title_header->setGeometry(W_TRACK_COL_TITLE_X, headerColY, W_TRACK_COL_TITLE_W, W_TRACK_HEADER_HEIGHT);
669 title_header->setFontColour(WZCOL_TEXT_MEDIUM);
670 title_header->setString(_("Title"));
671 title_header->setTextAlignment(WLAB_ALIGNTOPLEFT);
672 // set a custom hit-testing function that ignores all mouse input / clicks
673 title_header->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
674
675 auto album_header = std::make_shared<W_LABEL>();
676 pHeader->attach(album_header);
677 album_header->setGeometry(W_TRACK_COL_ALBUM_X, headerColY, W_TRACK_COL_ALBUM_W, W_TRACK_HEADER_HEIGHT);
678 album_header->setFontColour(WZCOL_TEXT_MEDIUM);
679 album_header->setString(_("Album"));
680 album_header->setTextAlignment(WLAB_ALIGNTOPLEFT);
681 // set a custom hit-testing function that ignores all mouse input / clicks
682 album_header->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
683
684 for (int musicModeIdx = 0; musicModeIdx < NUM_MUSICGAMEMODES; musicModeIdx++)
685 {
686 const std::string& headerImage = music_mode_col_header_images[musicModeIdx];
687 auto musicModeHeader = std::make_shared<W_MusicListHeaderColImage>();
688 pHeader->attach(musicModeHeader);
689 musicModeHeader->loadImage(headerImage);
690 musicModeHeader->setGeometry(W_TRACK_CHECKBOX_STARTINGPOS - 2 + ((W_TRACK_CHECKBOX_SIZE + W_TRACK_COL_PADDING) * musicModeIdx), headerColY, W_TRACK_HEADER_COL_IMAGE_SIZE, W_TRACK_HEADER_HEIGHT);
691 std::string musicGameModeStr = to_string(static_cast<MusicGameMode>(musicModeIdx));
692 musicModeHeader->setTip(musicGameModeStr);
693 }
694
695 auto pTracksScrollableList = ScrollableListWidget::make();
696 parent->attach(pTracksScrollableList);
697 pTracksScrollableList->setBackgroundColor(WZCOL_TRANSPARENT_BOX);
698 pTracksScrollableList->setCalcLayout([ingame](WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int){
699 psWidget->setGeometry(GetTrackListStartXPos(ingame), TL_Y, TL_ENTRYW, GetNumVisibleTracks() * TL_ENTRYH);
700 });
701
702 size_t tracksCount = trackList.size();
703 W_BUTINIT emptyInit;
704 for (size_t i = 0; i < tracksCount; i++)
705 {
706 auto pTrackRow = W_TrackRow::make(&emptyInit, trackList[i], ingame);
707 pTrackRow->setGeometry(0, 0, TL_ENTRYW, TL_ENTRYH);
708 pTrackRow->addOnClickHandler([](W_BUTTON& clickedButton) {
709 W_TrackRow& pTrackRow = dynamic_cast<W_TrackRow&>(clickedButton);
710 if (selectedTrack == pTrackRow.getTrack()) { return; }
711 cdAudio_PlaySpecificTrack(pTrackRow.getTrack());
712 });
713 pTracksScrollableList->addItem(pTrackRow);
714 }
715 }
716
717 // MARK: - "Now Playing" Details Block
718
closeMusicManager(bool ingame)719 static void closeMusicManager(bool ingame)
720 {
721 trackList.clear();
722 trackRowsCache.clear();
723 selectedTrack.reset();
724 if (musicManagerAudioEventSink)
725 {
726 musicManagerAudioEventSink->setUnregisterEventSink();
727 musicManagerAudioEventSink.reset();
728 }
729 if (!ingame)
730 {
731 cdAudio_PlayTrack(cdAudio_CurrentSongContext());
732 }
733
734 psNowPlaying = nullptr;
735 psSelectedTrackName = nullptr;
736 psSelectedTrackAuthorName = nullptr;
737 psSelectedTrackAlbumName = nullptr;
738 psSelectedTrackAlbumDate = nullptr;
739 psSelectedTrackAlbumDescription = nullptr;
740 }
741
742 class MusicManagerForm : public IntFormTransparent
743 {
744 protected:
MusicManagerForm(bool ingame)745 explicit MusicManagerForm(bool ingame)
746 : IntFormTransparent()
747 , ingame(ingame) {}
748
initialize()749 void initialize()
750 {
751 this->id = MM_FORM;
752 this->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
753 psWidget->setGeometry(0, 0, pie_GetVideoBufferWidth(), pie_GetVideoBufferHeight());
754 }));
755
756 // draws the background of the form
757 auto botForm = std::make_shared<IntFormAnimated>();
758 this->attach(botForm);
759 botForm->id = MM_FORM + 1;
760
761 if (!ingame)
762 {
763 botForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
764 psWidget->setGeometry(TL_X, 20, TL_W, GetTrackListHeight());
765 }));
766
767 // cancel
768 addMultiBut(*botForm, MM_RETURN, 10, 5, MULTIOP_OKW, MULTIOP_OKH, _("Return To Previous Screen"),
769 IMAGE_RETURN, IMAGE_RETURN_HI, IMAGE_RETURN_HI);
770 }
771 else
772 {
773 // Text versions for in-game where image resources are not available
774 botForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
775 psWidget->setGeometry(((300-(TL_W/2))+D_W), ((240-(GetTotalTrackAndDetailsBoxHeight()/2))+D_H), TL_W, GetTrackListHeight() + 20);
776 }));
777
778 addButton(botForm, _("Go Back"), MM_GO_BACK, GetTrackListHeight() - 20);
779 addButton(botForm, _("Resume Game"), MM_RETURN, GetTrackListHeight());
780 }
781
782 // get track list
783 trackList = PlayList_GetFullTrackList();
784 selectedTrack = cdAudio_GetCurrentTrack();
785
786 // register for cd audio events
787 musicManagerAudioEventSink = std::make_shared<MusicManager_CDAudioEventSink>();
788 cdAudio_RegisterForEvents(std::static_pointer_cast<CDAudioEventSink>(musicManagerAudioEventSink));
789
790 addTrackList(botForm.get(), ingame);
791 addTrackDetailsBox(this, ingame);
792 }
793
addButton(const std::shared_ptr<IntFormAnimated> & botForm,const char * text,int id,int y)794 static void addButton(const std::shared_ptr<IntFormAnimated> &botForm, const char *text, int id, int y)
795 {
796 W_BUTINIT sButInit;
797
798 sButInit.formID = botForm->id;
799 sButInit.style = WBUT_PLAIN | WBUT_TXTCENTRE;
800 sButInit.width = TL_W;
801 sButInit.FontID = font_regular;
802 sButInit.x = 0;
803 sButInit.height = 10;
804 sButInit.pDisplay = displayTextOption;
805 sButInit.initPUserDataFunc = []() -> void * { return new DisplayTextOptionCache(); };
806 sButInit.onDelete = [](WIDGET *psWidget) {
807 assert(psWidget->pUserData != nullptr);
808 delete static_cast<DisplayTextOptionCache *>(psWidget->pUserData);
809 psWidget->pUserData = nullptr;
810 };
811
812 sButInit.id = id;
813 sButInit.y = y;
814 sButInit.pText = text;
815 sButInit.calcLayout = [y] (WIDGET *psWidget, unsigned int, unsigned int, unsigned int, unsigned int) {
816 psWidget->move(0, y);
817 };
818
819 botForm->attach(std::make_shared<W_BUTTON>(&sButInit));
820 }
821
822 public:
make(bool ingame)823 static std::shared_ptr<MusicManagerForm> make(bool ingame)
824 {
825 class make_shared_enabler: public MusicManagerForm
826 {
827 public:
828 explicit make_shared_enabler(bool ingame): MusicManagerForm(ingame) {}
829 };
830 auto widget = std::make_shared<make_shared_enabler>(ingame);
831 widget->initialize();
832 return widget;
833 }
834
~MusicManagerForm()835 ~MusicManagerForm() override
836 {
837 closeMusicManager(ingame);
838 }
839 private:
840 bool ingame;
841 };
842
musicManager(WIDGET * parent,bool ingame)843 static bool musicManager(WIDGET *parent, bool ingame)
844 {
845 parent->attach(MusicManagerForm::make(ingame));
846 return true;
847 }
848
startInGameMusicManager()849 bool startInGameMusicManager()
850 {
851 bAllowOtherKeyPresses = false;
852 return musicManager(psWScreen->psForm.get(), true);
853 }
854
startMusicManager()855 bool startMusicManager()
856 {
857 addBackdrop(); //background image
858 addSideText(FRONTEND_SIDETEXT, TL_SX, 20, _("MUSIC MANAGER"));
859 WIDGET *parent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
860 return musicManager(parent, false);
861 }
862
perFrameCleanup()863 static void perFrameCleanup()
864 {
865 UDWORD currentFrameNum = frameGetFrameNumber();
866 for (auto i = trackRowsCache.begin(), last = trackRowsCache.end(); i != last; )
867 {
868 if (i->second->lastUsedFrameNumber != currentFrameNum)
869 {
870 i = trackRowsCache.erase(i);
871 }
872 else
873 {
874 ++i;
875 }
876 }
877 }
878
runInGameMusicManager(unsigned id)879 bool runInGameMusicManager(unsigned id)
880 {
881 if (id == MM_RETURN) // return
882 {
883 bAllowOtherKeyPresses = true;
884 widgDelete(psWScreen, MM_FORM);
885 inputLoseFocus();
886 return true;
887 }
888 else if (id == MM_GO_BACK)
889 {
890 bAllowOtherKeyPresses = true;
891 widgDelete(psWScreen, MM_FORM);
892 intReopenMenuWithoutUnPausing();
893 }
894
895 perFrameCleanup();
896
897 return false;
898 }
899
runMusicManager()900 bool runMusicManager()
901 {
902 WidgetTriggers const &triggers = widgRunScreen(psWScreen);
903 unsigned id = triggers.empty() ? 0 : triggers.front().widget->id; // Just use first click here, since the next click could be on another menu.
904
905 if (id == MM_RETURN) // return
906 {
907 changeTitleMode(OPTIONS);
908 }
909
910 widgDisplayScreen(psWScreen); // show the widgets currently running
911
912 perFrameCleanup();
913
914 if (CancelPressed())
915 {
916 changeTitleMode(OPTIONS);
917 }
918
919 return true;
920 }
921
922 // MARK: - CD Audio Event Sink Implementation
923
CDAudioEvent_UpdateCurrentTrack(const std::shared_ptr<const WZ_TRACK> & track)924 static void CDAudioEvent_UpdateCurrentTrack(const std::shared_ptr<const WZ_TRACK>& track)
925 {
926 if (selectedTrack == track)
927 {
928 return;
929 }
930 selectedTrack = track;
931 if (!psWScreen) { return; }
932 auto psTrackDetailsForm = dynamic_cast<TrackDetailsForm *>(widgGetFromID(psWScreen, MULTIOP_CONSOLEBOX));
933 if (psTrackDetailsForm)
934 {
935 UpdateTrackDetailsBox(psTrackDetailsForm);
936 }
937 }
938
startedPlayingTrack(const std::shared_ptr<const WZ_TRACK> & track)939 void MusicManager_CDAudioEventSink::startedPlayingTrack(const std::shared_ptr<const WZ_TRACK>& track)
940 {
941 CDAudioEvent_UpdateCurrentTrack(track);
942 }
trackEnded(const std::shared_ptr<const WZ_TRACK> & track)943 void MusicManager_CDAudioEventSink::trackEnded(const std::shared_ptr<const WZ_TRACK>& track)
944 {
945 // currently a no-op
946 }
947
musicStopped()948 void MusicManager_CDAudioEventSink::musicStopped()
949 {
950 CDAudioEvent_UpdateCurrentTrack(nullptr);
951 }
952
musicPaused(const std::shared_ptr<const WZ_TRACK> & track)953 void MusicManager_CDAudioEventSink::musicPaused(const std::shared_ptr<const WZ_TRACK>& track)
954 {
955 CDAudioEvent_UpdateCurrentTrack(track);
956 }
957
musicResumed(const std::shared_ptr<const WZ_TRACK> & track)958 void MusicManager_CDAudioEventSink::musicResumed(const std::shared_ptr<const WZ_TRACK>& track)
959 {
960 CDAudioEvent_UpdateCurrentTrack(track);
961 }
962