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