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  * @file init.c
21  *
22  * Game initialisation routines.
23  *
24  */
25 
26 #include "lib/framework/frame.h"
27 #include "notifications.h"
28 #include "lib/gamelib/gtime.h"
29 #include "lib/widget/form.h"
30 #include "lib/widget/button.h"
31 #include "lib/widget/label.h"
32 #include "lib/ivis_opengl/pieblitfunc.h"
33 #include "lib/ivis_opengl/screen.h"
34 #include "lib/ivis_opengl/pietypes.h"
35 #include "init.h"
36 #include "frend.h"
37 #include <algorithm>
38 #include <limits>
39 
40 // MARK: - Notification Ignore List
41 
42 #include <3rdparty/json/json.hpp>
43 using json = nlohmann::json;
44 
45 #include <typeinfo>
46 #include <physfs.h>
47 #include "lib/framework/file.h"
48 #include <sstream>
49 
50 class WZ_Notification_Preferences
51 {
52 public:
53 	WZ_Notification_Preferences(const std::string &fileName);
54 
55 	void incrementNotificationRuns(const std::string& uniqueNotificationIdentifier);
56 	uint32_t getNotificationRuns(const std::string& uniqueNotificationIdentifier) const;
57 
58 	void doNotShowNotificationAgain(const std::string& uniqueNotificationIdentifier);
59 	bool getDoNotShowNotificationValue(const std::string& uniqueNotificationIdentifier) const;
60 	bool canShowNotification(const WZ_Notification& notification) const;
61 
62 	void clearAllNotificationPreferences();
63 	bool removeNotificationPreferencesIf(const std::function<bool (const std::string& uniqueNotificationIdentifier)>& matchIdentifierFunc);
64 
65 	bool savePreferences();
66 
67 private:
68 	void storeValueForNotification(const std::string& uniqueNotificationIdentifier, const std::string& key, const json& value);
69 	json getJSONValueForNotification(const std::string& uniqueNotificationIdentifier, const std::string& key, const json& defaultValue = json()) const;
70 
71 	template<typename T>
getValueForNotification(const std::string & uniqueNotificationIdentifier,const std::string & key,T defaultValue) const72 	T getValueForNotification(const std::string& uniqueNotificationIdentifier, const std::string& key, T defaultValue) const
73 	{
74 		T result = defaultValue;
75 		try {
76 			result = getJSONValueForNotification(uniqueNotificationIdentifier, key, json(defaultValue)).get<T>();
77 		}
78 		catch (const std::exception &e) {
79 			debug(LOG_WARNING, "Failed to convert json_variant to %s because of error: %s", typeid(T).name(), e.what());
80 			result = defaultValue;
81 		}
82 		catch (...) {
83 			debug(LOG_FATAL, "Unexpected exception encountered: json_variant::toType<%s>", typeid(T).name());
84 		}
85 		return result;
86 	}
87 
88 private:
89 	json mRoot;
90 	std::string mFilename;
91 };
92 
WZ_Notification_Preferences(const std::string & fileName)93 WZ_Notification_Preferences::WZ_Notification_Preferences(const std::string &fileName)
94 : mFilename(fileName)
95 {
96 	if (PHYSFS_exists(fileName.c_str()))
97 	{
98 		UDWORD size;
99 		char *data;
100 		if (loadFile(fileName.c_str(), &data, &size))
101 		{
102 			try {
103 				mRoot = json::parse(data, data + size);
104 			}
105 			catch (const std::exception &e) {
106 				ASSERT(false, "JSON document from %s is invalid: %s", fileName.c_str(), e.what());
107 			}
108 			catch (...) {
109 				debug(LOG_ERROR, "Unexpected exception parsing JSON %s", fileName.c_str());
110 			}
111 			ASSERT(!mRoot.is_null(), "JSON document from %s is null", fileName.c_str());
112 			ASSERT(mRoot.is_object(), "JSON document from %s is not an object. Read: \n%s", fileName.c_str(), data);
113 			free(data);
114 		}
115 		else
116 		{
117 			debug(LOG_ERROR, "Could not open \"%s\"", fileName.c_str());
118 			// treat as if no preferences exist yet
119 			mRoot = json::object();
120 		}
121 	}
122 	else
123 	{
124 		// no preferences exist yet
125 		mRoot = json::object();
126 	}
127 
128 	// always ensure there's a "notifications" dictionary in the root object
129 	auto notificationsObject = mRoot.find("notifications");
130 	if (notificationsObject == mRoot.end() || !notificationsObject->is_object())
131 	{
132 		// create a dictionary object
133 		mRoot["notifications"] = json::object();
134 	}
135 }
136 
storeValueForNotification(const std::string & uniqueNotificationIdentifier,const std::string & key,const json & value)137 void WZ_Notification_Preferences::storeValueForNotification(const std::string& uniqueNotificationIdentifier, const std::string& key, const json& value)
138 {
139 	json& notificationsObj = mRoot["notifications"];
140 	auto notificationData = notificationsObj.find(uniqueNotificationIdentifier);
141 	if (notificationData == notificationsObj.end() || !notificationData->is_object())
142 	{
143 		notificationsObj[uniqueNotificationIdentifier] = json::object();
144 		notificationData = notificationsObj.find(uniqueNotificationIdentifier);
145 	}
146 	(*notificationData)[key] = value;
147 }
148 
getJSONValueForNotification(const std::string & uniqueNotificationIdentifier,const std::string & key,const json & defaultValue) const149 json WZ_Notification_Preferences::getJSONValueForNotification(const std::string& uniqueNotificationIdentifier, const std::string& key, const json& defaultValue /*= json()*/) const
150 {
151 	try {
152 		return mRoot.at("notifications").at(uniqueNotificationIdentifier).at(key);
153 	}
154 	catch (json::out_of_range&)
155 	{
156 		// some part of the path doesn't exist yet - return the default value
157 		return defaultValue;
158 	}
159 }
160 
incrementNotificationRuns(const std::string & uniqueNotificationIdentifier)161 void WZ_Notification_Preferences::incrementNotificationRuns(const std::string& uniqueNotificationIdentifier)
162 {
163 	uint32_t seenCount = getNotificationRuns(uniqueNotificationIdentifier);
164 	storeValueForNotification(uniqueNotificationIdentifier, "seen", ++seenCount);
165 }
166 
getNotificationRuns(const std::string & uniqueNotificationIdentifier) const167 uint32_t WZ_Notification_Preferences::getNotificationRuns(const std::string& uniqueNotificationIdentifier) const
168 {
169 	return getValueForNotification(uniqueNotificationIdentifier, "seen", 0);
170 }
171 
doNotShowNotificationAgain(const std::string & uniqueNotificationIdentifier)172 void WZ_Notification_Preferences::doNotShowNotificationAgain(const std::string& uniqueNotificationIdentifier)
173 {
174 	storeValueForNotification(uniqueNotificationIdentifier, "skip", true);
175 }
176 
getDoNotShowNotificationValue(const std::string & uniqueNotificationIdentifier) const177 bool WZ_Notification_Preferences::getDoNotShowNotificationValue(const std::string& uniqueNotificationIdentifier) const
178 {
179 	return getValueForNotification(uniqueNotificationIdentifier, "skip", false);
180 }
181 
clearAllNotificationPreferences()182 void WZ_Notification_Preferences::clearAllNotificationPreferences()
183 {
184 	mRoot["notifications"] = json::object();
185 }
186 
removeNotificationPreferencesIf(const std::function<bool (const std::string & uniqueNotificationIdentifier)> & matchIdentifierFunc)187 bool WZ_Notification_Preferences::removeNotificationPreferencesIf(const std::function<bool (const std::string& uniqueNotificationIdentifier)>& matchIdentifierFunc)
188 {
189 	ASSERT_OR_RETURN(false, mRoot.contains("notifications"), "root missing notifications object");
190 	json notificationsObjCopy = mRoot.at("notifications");
191 	std::vector<std::string> identifiersToRemove;
192 	for (auto it : notificationsObjCopy.items())
193 	{
194 		const auto& uniqueNotificationIdentifier = it.key();
195 		if (matchIdentifierFunc(uniqueNotificationIdentifier))
196 		{
197 			identifiersToRemove.push_back(uniqueNotificationIdentifier);
198 		}
199 	}
200 	if (identifiersToRemove.empty())
201 	{
202 		return false;
203 	}
204 	for (const auto& uniqueNotificationIdentifier : identifiersToRemove)
205 	{
206 		notificationsObjCopy.erase(uniqueNotificationIdentifier);
207 	}
208 	mRoot["notifications"] = notificationsObjCopy;
209 	return true;
210 }
211 
savePreferences()212 bool WZ_Notification_Preferences::savePreferences()
213 {
214 	std::ostringstream stream;
215 	stream << mRoot.dump(4) << std::endl;
216 	std::string jsonString = stream.str();
217 	saveFile(mFilename.c_str(), jsonString.c_str(), jsonString.size());
218 	return true;
219 }
220 
canShowNotification(const WZ_Notification & notification) const221 bool WZ_Notification_Preferences::canShowNotification(const WZ_Notification& notification) const
222 {
223 	if (notification.displayOptions.uniqueNotificationIdentifier().empty())
224 	{
225 		return true;
226 	}
227 
228 	bool suppressNotification = false;
229 	if (notification.displayOptions.isOneTimeNotification())
230 	{
231 		suppressNotification = (getNotificationRuns(notification.displayOptions.uniqueNotificationIdentifier()) > 0);
232 	}
233 	else
234 	{
235 		suppressNotification = getDoNotShowNotificationValue(notification.displayOptions.uniqueNotificationIdentifier());
236 	}
237 
238 	return !suppressNotification;
239 }
240 
241 static WZ_Notification_Preferences* notificationPrefs = nullptr;
242 
243 // MARK: - In-Game Notification System Types
244 
245 class WZ_Notification_Status
246 {
247 public:
WZ_Notification_Status(uint32_t queuedTime)248 	WZ_Notification_Status(uint32_t queuedTime)
249 	: queuedTime(queuedTime)
250 	{ }
251 	enum NotificationState
252 	{
253 		waiting,
254 		opening,
255 		shown,
256 		closing,
257 		closed
258 	};
259 	NotificationState state = NotificationState::waiting;
260 	uint32_t stateStartTime = 0;
261 	uint32_t queuedTime = 0;
262 	float animationSpeed = 1.0f;
263 public:
264 	// normal speed is 1.0
setAnimationSpeed(float newSpeed)265 	void setAnimationSpeed(float newSpeed)
266 	{
267 		animationSpeed = newSpeed;
268 	}
269 };
270 
271 class WZ_Queued_Notification
272 {
273 public:
WZ_Queued_Notification(const WZ_Notification & notification,const WZ_Notification_Status & status,const WZ_Notification_Trigger & trigger)274 	WZ_Queued_Notification(const WZ_Notification& notification, const WZ_Notification_Status& status, const WZ_Notification_Trigger& trigger)
275 	: notification(notification)
276 	, status(status)
277 	, trigger(trigger)
278 	{ }
279 
280 public:
281 	void setState(WZ_Notification_Status::NotificationState newState);
wasProgrammaticallyDismissed() const282 	bool wasProgrammaticallyDismissed() const { return dismissalCause == WZ_Notification_Dismissal_Reason::ACTION_BUTTON_CLICK || dismissalCause == WZ_Notification_Dismissal_Reason::PROGRAMMATIC; }
dismissReason() const283 	WZ_Notification_Dismissal_Reason dismissReason() const { return dismissalCause; }
284 
285 protected:
setWasProgrammaticallyDismissed()286 	void setWasProgrammaticallyDismissed() { dismissalCause = WZ_Notification_Dismissal_Reason::PROGRAMMATIC; }
setWasDismissedByActionButton()287 	void setWasDismissedByActionButton() { dismissalCause = WZ_Notification_Dismissal_Reason::ACTION_BUTTON_CLICK; }
288 
289 public:
290 	WZ_Notification notification;
291 	WZ_Notification_Status status;
292 	WZ_Notification_Trigger trigger;
293 private:
294 	bool bWasInitiallyShown = false;
295 	bool bWasFullyShown = false;
296 	WZ_Notification_Dismissal_Reason dismissalCause = WZ_Notification_Dismissal_Reason::USER_DISMISSED;
297 	friend class W_NOTIFICATION;
298 };
299 
setState(WZ_Notification_Status::NotificationState newState)300 void WZ_Queued_Notification::setState(WZ_Notification_Status::NotificationState newState)
301 {
302 	status.state = newState;
303 	status.stateStartTime = realTime;
304 
305 	if (newState == WZ_Notification_Status::NotificationState::closed)
306 	{
307 		if (notification.isIgnorable() && !bWasFullyShown && (dismissalCause != WZ_Notification_Dismissal_Reason::PROGRAMMATIC))
308 		{
309 			notificationPrefs->incrementNotificationRuns(notification.displayOptions.uniqueNotificationIdentifier());
310 		}
311 		bWasFullyShown = true;
312 	}
313 	else if (newState == WZ_Notification_Status::NotificationState::shown)
314 	{
315 		if (!bWasInitiallyShown)
316 		{
317 			bWasInitiallyShown = true;
318 			if (notification.onDisplay)
319 			{
320 				notification.onDisplay(notification);
321 			}
322 		}
323 	}
324 }
325 
326 // MARK: - In-Game Notification Widgets
327 
328 // Forward-declarations
329 void notificationsDidStartDragOnNotification(const Vector2i& dragStartPos);
330 void notificationDidStopDragOnNotification();
331 void finishedProcessingNotificationRequest(WZ_Queued_Notification* request, bool doNotShowAgain);
332 void removeInGameNotificationForm(WZ_Queued_Notification* request);
333 
334 #include "lib/framework/input.h"
335 
336 #define WZ_NOTIFICATION_WIDTH 500
337 #define WZ_NOTIFICATION_PADDING	15
338 #define WZ_NOTIFICATION_IMAGE_SIZE	36
339 #define WZ_NOTIFICATION_CONTENTS_LINE_SPACING	0
340 #define WZ_NOTIFICATION_CONTENTS_TOP_PADDING	5
341 #define WZ_NOTIFICATION_BUTTON_HEIGHT 20
342 #define WZ_NOTIFICATION_BETWEEN_BUTTON_PADDING	10
343 
344 #define WZ_NOTIFY_DONOTSHOWAGAINCB_ID 5
345 
346 #include "hci.h"
347 #include "intfac.h"
348 #include "intdisplay.h"
349 
350 #ifndef GLM_ENABLE_EXPERIMENTAL
351 	#define GLM_ENABLE_EXPERIMENTAL
352 #endif
353 #include <glm/gtx/transform.hpp>
354 #include "lib/ivis_opengl/pieblitfunc.h"
355 
356 struct WzCheckboxButton : public W_BUTTON
357 {
358 public:
WzCheckboxButtonWzCheckboxButton359 	WzCheckboxButton() : W_BUTTON()
360 	{
361 		addOnClickHandler([](W_BUTTON& button) {
362 			WzCheckboxButton& self = static_cast<WzCheckboxButton&>(button);
363 			self.isChecked = !self.isChecked;
364 		});
365 	}
366 
367 	void display(int xOffset, int yOffset);
368 
369 	Vector2i calculateDesiredDimensions();
370 
getIsCheckedWzCheckboxButton371 	bool getIsChecked() const { return isChecked; }
372 private:
checkboxSizeWzCheckboxButton373 	int checkboxSize()
374 	{
375 		wzText.setText(pText.toUtf8(), FontID);
376 		return wzText.lineSize() - 2;
377 	}
378 private:
379 	WzText wzText;
380 	bool isChecked = false;
381 };
382 
MakeNotificationFormInit()383 static W_FORMINIT MakeNotificationFormInit()
384 {
385 	W_FORMINIT sFormInit;
386 	sFormInit.formID = 0;
387 	sFormInit.id = 0;
388 	sFormInit.x = 0; // always base of 0
389 	sFormInit.y = 0; // always base of 0
390 	sFormInit.width = WZ_NOTIFICATION_WIDTH; // fixed width
391 	sFormInit.height = 100; // starting height - real height is calculated later based on layout of contents
392 	return sFormInit;
393 }
394 
395 class W_NOTIFICATION : public W_FORM
396 {
397 protected:
398 	W_NOTIFICATION(WZ_Queued_Notification* request, W_FORMINIT init = MakeNotificationFormInit());
399 public:
400 	~W_NOTIFICATION();
401 	static std::shared_ptr<W_NOTIFICATION> make(WZ_Queued_Notification* request, W_FORMINIT init = MakeNotificationFormInit());
402 	void run(W_CONTEXT *psContext) override;
403 	void clicked(W_CONTEXT *psContext, WIDGET_KEY key) override;
404 	void released(W_CONTEXT *psContext, WIDGET_KEY key) override;
405 	void display(int xOffset, int yOffset) override;
406 public:
getDragOffset() const407 	Vector2i getDragOffset() const { return dragOffset; }
isActivelyBeingDragged() const408 	bool isActivelyBeingDragged() const { return isInDragMode; }
getLastDragStartTime() const409 	uint32_t getLastDragStartTime() const { return dragStartedTime; }
getLastDragEndTime() const410 	uint32_t getLastDragEndTime() const { return dragEndedTime; }
getLastDragDuration() const411 	uint32_t getLastDragDuration() const { return dragEndedTime - dragStartedTime; }
notificationTag() const412 	const std::string& notificationTag() const { return request->notification.tag; }
413 public:
414 	// to be called from the larger In-Game Notifications System
415 	bool dismissNotification(float animationSpeed = 1.0f);
416 private:
417 	bool calculateNotificationWidgetPos();
418 	gfx_api::texture* loadImage(const WZ_Notification_Image& image);
419 	void internalDismissNotification(float animationSpeed = 1.0f);
420 public:
421 	std::shared_ptr<WzCheckboxButton> pOnDoNotShowAgainCheckbox = nullptr;
422 private:
423 	WZ_Queued_Notification* request;
424 	bool isInDragMode = false;
425 	Vector2i dragOffset = {0, 0};
426 	Vector2i dragStartMousePos = {0, 0};
427 	Vector2i dragOffsetEnded = {0, 0};
428 	uint32_t dragStartedTime = 0;
429 	uint32_t dragEndedTime = 0;
430 	gfx_api::texture* pImageTexture = nullptr;
431 };
432 
433 struct DisplayNotificationButtonCache
434 {
435 	WzText wzText;
436 };
437 
438 // MARK: - In-game Notification Display
439 
440 static std::list<std::unique_ptr<WZ_Queued_Notification>> notificationQueue;
441 static std::shared_ptr<W_SCREEN> psNotificationOverlayScreen = nullptr;
442 static std::unique_ptr<WZ_Queued_Notification> currentNotification;
443 static std::shared_ptr<W_NOTIFICATION> currentInGameNotification = nullptr;
444 static uint32_t lastNotificationClosed = 0;
445 static Vector2i lastDragOnNotificationStartPos(-1,-1);
446 
popNextQueuedNotification()447 std::unique_ptr<WZ_Queued_Notification> popNextQueuedNotification()
448 {
449 	ASSERT(notificationPrefs, "Notification preferences not loaded!");
450 	auto it = notificationQueue.begin();
451 	while (it != notificationQueue.end())
452 	{
453 		auto & request = *it;
454 
455 		if (!notificationPrefs->canShowNotification(request->notification))
456 		{
457 			// ignore this notification - remove from the list
458 			debug(LOG_GUI, "Ignoring notification: %s", request->notification.displayOptions.uniqueNotificationIdentifier().c_str());
459 			if (request->notification.onIgnored)
460 			{
461 				request->notification.onIgnored(request->notification);
462 			}
463 			it = notificationQueue.erase(it);
464 			continue;
465 		}
466 
467 		uint32_t num = std::min<uint32_t>(realTime - request->status.queuedTime, request->trigger.timeInterval);
468 		if (num == request->trigger.timeInterval)
469 		{
470 			std::unique_ptr<WZ_Queued_Notification> retVal(std::move(request));
471 			it = notificationQueue.erase(it);
472 			return retVal;
473 		}
474 
475 		++it;
476 	}
477 	return nullptr;
478 }
479 
480 // MARK: - WzCheckboxButton
481 
calculateDesiredDimensions()482 Vector2i WzCheckboxButton::calculateDesiredDimensions()
483 {
484 	int cbSize = checkboxSize();
485 	Vector2i checkboxPos{x(), y()};
486 	int textLeftPos = checkboxPos.x + cbSize + 7;
487 
488 	// TODO: Incorporate padding?
489 	return Vector2i(textLeftPos + wzText.width(), std::max(wzText.lineSize(), cbSize));
490 }
491 
display(int xOffset,int yOffset)492 void WzCheckboxButton::display(int xOffset, int yOffset)
493 {
494 	wzText.setText(pText.toUtf8(), FontID);
495 
496 	int x0 = xOffset + x();
497 	int y0 = yOffset + y();
498 
499 	bool down = (getState() & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0;
500 	bool grey = (getState() & WBUT_DISABLE) != 0;
501 
502 	// calculate checkbox dimensions
503 	int cbSize = checkboxSize();
504 	Vector2i checkboxOffset{0, (height() - cbSize) / 2}; // left-align, center vertically
505 	Vector2i checkboxPos{x0 + checkboxOffset.x, y0 + checkboxOffset.y};
506 
507 	// draw checkbox border
508 	PIELIGHT notifyBoxAddColor = WZCOL_NOTIFICATION_BOX;
509 	notifyBoxAddColor.byte.a = uint8_t(float(notifyBoxAddColor.byte.a) * 0.7f);
510 	pie_UniTransBoxFill(checkboxPos.x, checkboxPos.y, checkboxPos.x + cbSize, checkboxPos.y + cbSize, notifyBoxAddColor);
511 	iV_Box2(checkboxPos.x, checkboxPos.y, checkboxPos.x + cbSize, checkboxPos.y + cbSize, WZCOL_TEXT_MEDIUM, WZCOL_TEXT_MEDIUM);
512 
513 	if (down || isChecked)
514 	{
515 		// draw checkbox "checked" inside
516 		#define CB_INNER_INSET 2
517 		PIELIGHT checkBoxInsideColor = WZCOL_TEXT_MEDIUM;
518 		checkBoxInsideColor.byte.a = 200;
519 		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);
520 	}
521 
522 	if (grey)
523 	{
524 		// disabled, render something over it!
525 		iV_TransBoxFill(x0, y0, x0 + width(), y0 + height());
526 	}
527 
528 	// Display text to the right of the checkbox image
529 	int textLeftPos = checkboxPos.x + cbSize + 7;
530 	int fx = textLeftPos;
531 	int fw = wzText.width();
532 	int fy = yOffset + y() + (height() - wzText.lineSize()) / 2 - wzText.aboveBase();
533 
534 	if (style & WBUT_TXTCENTRE)							//check for centering, calculate offset.
535 	{
536 		fx = textLeftPos + ((width() - fw) / 2);
537 	}
538 
539 	wzText.render(fx, fy, WZCOL_TEXT_MEDIUM);
540 }
541 
542 // MARK: - W_NOTIFICATION
543 
544 // ////////////////////////////////////////////////////////////////////////////
545 // display a notification action button
displayNotificationAction(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)546 void displayNotificationAction(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
547 {
548 	SDWORD			fx, fy, fw;
549 	W_BUTTON		*psBut = (W_BUTTON *)psWidget;
550 	bool			hilight = false;
551 	bool			greyOut = /*psWidget->UserData ||*/ (psBut->getState() & WBUT_DISABLE); // if option is unavailable.
552 	bool			isActionButton = (psBut->UserData == 1);
553 
554 	// Any widget using displayTextOption must have its pUserData initialized to a (DisplayTextOptionCache*)
555 	assert(psWidget->pUserData != nullptr);
556 	DisplayNotificationButtonCache& cache = *static_cast<DisplayNotificationButtonCache*>(psWidget->pUserData);
557 
558 	cache.wzText.setText(psBut->pText.toUtf8(), psBut->FontID);
559 
560 	if (psBut->isHighlighted())					// if mouse is over text then hilight.
561 	{
562 		hilight = true;
563 	}
564 
565 	PIELIGHT colour;
566 
567 	if (greyOut)								// unavailable
568 	{
569 		colour = WZCOL_TEXT_DARK;
570 	}
571 	else										// available
572 	{
573 		if (hilight || isActionButton)			// hilight
574 		{
575 			colour = WZCOL_TEXT_BRIGHT;
576 		}
577 		else									// don't highlight
578 		{
579 			colour = WZCOL_TEXT_MEDIUM;
580 		}
581 	}
582 
583 	if (isActionButton)
584 	{
585 		// "Action" buttons have a bordering box
586 		int x0 = psBut->x() + xOffset;
587 		int y0 = psBut->y() + yOffset;
588 		int x1 = x0 + psBut->width();
589 		int y1 = y0 + psBut->height();
590 		if (hilight)
591 		{
592 			PIELIGHT fillClr = pal_RGBA(255, 255, 255, 30);
593 			pie_UniTransBoxFill(x0, y0, x1, y1, fillClr);
594 		}
595 		iV_Box(x0, y0, x1, y1, colour);
596 	}
597 
598 	fw = cache.wzText.width();
599 	fy = yOffset + psWidget->y() + (psWidget->height() - cache.wzText.lineSize()) / 2 - cache.wzText.aboveBase();
600 
601 	if (psWidget->style & WBUT_TXTCENTRE)							//check for centering, calculate offset.
602 	{
603 		fx = xOffset + psWidget->x() + ((psWidget->width() - fw) / 2);
604 	}
605 	else
606 	{
607 		fx = xOffset + psWidget->x();
608 	}
609 
610 	if (!greyOut)
611 	{
612 		cache.wzText.render(fx + 1, fy + 1, pal_RGBA(0, 0, 0, 80));
613 	}
614 	cache.wzText.render(fx, fy, colour);
615 
616 	return;
617 }
618 
dismissNotification(float animationSpeed)619 bool W_NOTIFICATION::dismissNotification(float animationSpeed /*= 1.0f*/)
620 {
621 	switch (request->status.state)
622 	{
623 		case WZ_Notification_Status::NotificationState::closing:
624 		case WZ_Notification_Status::NotificationState::closed:
625 			// do nothing
626 			break;
627 		default:
628 			request->setWasProgrammaticallyDismissed();
629 			request->status.setAnimationSpeed(animationSpeed);
630 			request->setState(WZ_Notification_Status::NotificationState::closing);
631 			return true;
632 			break;
633 	}
634 
635 	return false;
636 }
637 
internalDismissNotification(float animationSpeed)638 void W_NOTIFICATION::internalDismissNotification(float animationSpeed /*= 1.0f*/)
639 {
640 	// if notification is the one being displayed, animate it away by setting its state to closing
641 	switch (request->status.state)
642 	{
643 		case WZ_Notification_Status::NotificationState::waiting:
644 			// should not happen
645 			break;
646 ////		case WZ_Notification_Status::NotificationState::opening:
647 //			request->setState(WZ_Notification_Status::NotificationState::shown);
648 //			break;
649 		case WZ_Notification_Status::NotificationState::shown:
650 			request->status.setAnimationSpeed(animationSpeed);
651 			request->setState(WZ_Notification_Status::NotificationState::closing);
652 			break;
653 		default:
654 			// do nothing
655 			break;
656 	}
657 }
658 
W_NOTIFICATION(WZ_Queued_Notification * request,W_FORMINIT init)659 W_NOTIFICATION::W_NOTIFICATION(WZ_Queued_Notification* request, W_FORMINIT init /*= MakeNotificationFormInit()*/)
660 : W_FORM(&init)
661 , request(request)
662 { }
663 
make(WZ_Queued_Notification * request,W_FORMINIT init)664 std::shared_ptr<W_NOTIFICATION> W_NOTIFICATION::make(WZ_Queued_Notification* request, W_FORMINIT init /*= MakeNotificationFormInit()*/)
665 {
666 	class make_shared_enabler : public W_NOTIFICATION {
667 	public:
668 		make_shared_enabler(WZ_Queued_Notification* request, W_FORMINIT init): W_NOTIFICATION(request, init) {}
669 	};
670 	auto psNewNotificationForm = std::make_shared<make_shared_enabler>(request, init);
671 	psNotificationOverlayScreen->psForm->attach(psNewNotificationForm);
672 
673 	// Load the image, if specified
674 	if (!request->notification.largeIcon.empty())
675 	{
676 		psNewNotificationForm->pImageTexture = psNewNotificationForm->loadImage(request->notification.largeIcon);
677 	}
678 
679 	const int notificationWidth = psNewNotificationForm->width();
680 
681 //		/* Add the close button */
682 //		W_BUTINIT sCloseButInit;
683 //		sCloseButInit.formID = 0;
684 //		sCloseButInit.id = 1;
685 //		sCloseButInit.pTip = _("Close");
686 //		sCloseButInit.pDisplay = intDisplayImageHilight;
687 //		sCloseButInit.UserData = PACKDWORD_TRI(0, IMAGE_CLOSEHILIGHT , IMAGE_CLOSE);
688 //		W_BUTTON* psCloseButton = new W_BUTTON(&sCloseButInit);
689 //		psCloseButton->addOnClickHandler([psNewNotificationForm](W_BUTTON& button) {
690 //			psNewNotificationForm->internalDismissNotification();
691 //		});
692 //		attach(psCloseButton);
693 //		psCloseButton->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
694 //			int parentWidth = psWidget->parent()->width();
695 //			psWidget->setGeometry(parentWidth - CLOSE_WIDTH, 0, CLOSE_WIDTH, CLOSE_HEIGHT);
696 //		}));
697 
698 	// Calculate dimensions for text area
699 	int imageSize = (psNewNotificationForm->pImageTexture) ? WZ_NOTIFICATION_IMAGE_SIZE : 0;
700 	int maxTextWidth = notificationWidth - (WZ_NOTIFICATION_PADDING * 2) - imageSize - ((imageSize > 0) ? WZ_NOTIFICATION_PADDING : 0);
701 
702 	// Add title
703 	auto label_title = std::make_shared<W_LABEL>();
704 	psNewNotificationForm->attach(label_title);
705 	label_title->setGeometry(WZ_NOTIFICATION_PADDING, WZ_NOTIFICATION_PADDING, maxTextWidth, 12);
706 	label_title->setFontColour(WZCOL_TEXT_BRIGHT);
707 	int heightOfTitleLabel = label_title->setFormattedString(WzString::fromUtf8(request->notification.contentTitle), maxTextWidth, font_regular_bold, WZ_NOTIFICATION_CONTENTS_LINE_SPACING);
708 	label_title->setGeometry(label_title->x(), label_title->y(), maxTextWidth, heightOfTitleLabel);
709 	label_title->setTextAlignment(WLAB_ALIGNTOPLEFT);
710 	// set a custom hit-testing function that ignores all mouse input / clicks
711 	label_title->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
712 
713 	// Add contents
714 	auto label_contents = std::make_shared<W_LABEL>();
715 	psNewNotificationForm->attach(label_contents);
716 //	debug(LOG_GUI, "label_title.height=%d", label_title->height());
717 	label_contents->setGeometry(WZ_NOTIFICATION_PADDING, WZ_NOTIFICATION_PADDING + label_title->height() + WZ_NOTIFICATION_CONTENTS_TOP_PADDING, maxTextWidth, 12);
718 	label_contents->setFontColour(WZCOL_TEXT_BRIGHT);
719 	int heightOfContentsLabel = label_contents->setFormattedString(WzString::fromUtf8(request->notification.contentText), maxTextWidth, font_regular, WZ_NOTIFICATION_CONTENTS_LINE_SPACING);
720 	label_contents->setGeometry(label_contents->x(), label_contents->y(), maxTextWidth, heightOfContentsLabel);
721 	label_contents->setTextAlignment(WLAB_ALIGNTOPLEFT);
722 	// set a custom hit-testing function that ignores all mouse input / clicks
723 	label_contents->setCustomHitTest([](WIDGET *psWidget, int x, int y) -> bool { return false; });
724 
725 	// Add action buttons
726 	std::string dismissLabel = _("Dismiss");
727 	std::string actionLabel = request->notification.action.title;
728 
729 	std::shared_ptr<W_BUTTON> psActionButton = nullptr;
730 	std::shared_ptr<W_BUTTON> psDismissButton = nullptr;
731 
732 	// Position the buttons below the text contents area
733 	int buttonsTop = label_contents->y() + label_contents->height() + WZ_NOTIFICATION_PADDING;
734 
735 	W_BUTINIT sButInit;
736 	sButInit.formID = 0;
737 	sButInit.height = WZ_NOTIFICATION_BUTTON_HEIGHT;
738 	sButInit.y = buttonsTop;
739 	sButInit.style |= WBUT_TXTCENTRE;
740 	sButInit.UserData = 0; // store whether "Action" button or not
741 	sButInit.initPUserDataFunc = []() -> void * { return new DisplayNotificationButtonCache(); };
742 	sButInit.onDelete = [](WIDGET *psWidget) {
743 		assert(psWidget->pUserData != nullptr);
744 		delete static_cast<DisplayNotificationButtonCache *>(psWidget->pUserData);
745 		psWidget->pUserData = nullptr;
746 	};
747 	sButInit.pDisplay = displayNotificationAction;
748 
749 	if (!actionLabel.empty())
750 	{
751 		// Display both an "Action" button and a "Dismiss" button
752 
753 		// 1.) "Action" button
754 		sButInit.id = 2;
755 		sButInit.width = iV_GetTextWidth(actionLabel.c_str(), font_regular_bold) + 18;
756 		sButInit.x = (short)(psNewNotificationForm->width() - WZ_NOTIFICATION_PADDING - sButInit.width);
757 		sButInit.UserData = 1; // store "Action" state
758 		sButInit.FontID = font_regular_bold;
759 		sButInit.pText = actionLabel.c_str();
760 		psActionButton = std::make_shared<W_BUTTON>(&sButInit);
761 		psNewNotificationForm->attach(psActionButton);
762 		psActionButton->addOnClickHandler([](W_BUTTON& button) {
763 			auto psNewNotificationForm = std::dynamic_pointer_cast<W_NOTIFICATION>(button.parent());
764 			ASSERT_OR_RETURN(, psNewNotificationForm != nullptr, "null parent");
765 			if (psNewNotificationForm->request->notification.action.onAction)
766 			{
767 				psNewNotificationForm->request->notification.action.onAction(psNewNotificationForm->request->notification);
768 			}
769 			else
770 			{
771 				debug(LOG_ERROR, "Action defined (\"%s\"), but no action handler!", psNewNotificationForm->request->notification.action.title.c_str());
772 			}
773 			psNewNotificationForm->request->setWasDismissedByActionButton();
774 			psNewNotificationForm->internalDismissNotification();
775 		});
776 	}
777 
778 	if (psActionButton != nullptr || request->notification.duration == 0)
779 	{
780 		// 2.) "Dismiss" button
781 		dismissLabel = u8"▴ " + dismissLabel;
782 		sButInit.id = 3;
783 		sButInit.FontID = font_regular;
784 		sButInit.width = iV_GetTextWidth(dismissLabel.c_str(), font_regular) + 18;
785 		sButInit.x = (short)(((psActionButton) ? (psActionButton->x()) - WZ_NOTIFICATION_BETWEEN_BUTTON_PADDING : psNewNotificationForm->width() - WZ_NOTIFICATION_PADDING) - sButInit.width);
786 		sButInit.pText = dismissLabel.c_str();
787 		sButInit.UserData = 0; // store regular state
788 		psDismissButton = std::make_shared<W_BUTTON>(&sButInit);
789 		psDismissButton->addOnClickHandler([](W_BUTTON& button) {
790 			auto psNewNotificationForm = std::dynamic_pointer_cast<W_NOTIFICATION>(button.parent());
791 			ASSERT_OR_RETURN(, psNewNotificationForm != nullptr, "null parent");
792 			psNewNotificationForm->internalDismissNotification();
793 		});
794 		psNewNotificationForm->attach(psDismissButton);
795 	}
796 
797 	if (request->notification.isIgnorable() && !request->notification.displayOptions.isOneTimeNotification())
798 	{
799 		ASSERT(notificationPrefs, "Notification preferences not loaded!");
800 		auto numTimesShown = notificationPrefs->getNotificationRuns(request->notification.displayOptions.uniqueNotificationIdentifier());
801 		if (numTimesShown >= request->notification.displayOptions.numTimesSeenBeforeDoNotShowAgainOption())
802 		{
803 			// Display "do not show again" button with checkbox
804 			auto pDoNotShowAgainButton = std::make_shared<WzCheckboxButton>();
805 			psNewNotificationForm->attach(pDoNotShowAgainButton);
806 			pDoNotShowAgainButton->id = WZ_NOTIFY_DONOTSHOWAGAINCB_ID;
807 			pDoNotShowAgainButton->pText = _("Do not show again");
808 			pDoNotShowAgainButton->FontID = font_small;
809 			Vector2i minimumDimensions = pDoNotShowAgainButton->calculateDesiredDimensions();
810 			pDoNotShowAgainButton->setGeometry(WZ_NOTIFICATION_PADDING, buttonsTop, minimumDimensions.x, std::max(minimumDimensions.y, WZ_NOTIFICATION_BUTTON_HEIGHT));
811 			psNewNotificationForm->pOnDoNotShowAgainCheckbox = pDoNotShowAgainButton;
812 		}
813 	}
814 
815 	// calculate the required height for the notification
816 	int bottom_labelContents = label_contents->y() + label_contents->height();
817 	uint16_t calculatedHeight = bottom_labelContents + WZ_NOTIFICATION_PADDING;
818 	if (psActionButton || psDismissButton)
819 	{
820 		int maxButtonBottom = std::max<int>((psActionButton) ? (psActionButton->y() + psActionButton->height()) : 0, (psDismissButton) ? (psDismissButton->y() + psDismissButton->height()) : 0);
821 		calculatedHeight = std::max<int>(calculatedHeight, maxButtonBottom + WZ_NOTIFICATION_PADDING);
822 	}
823 	// Also factor in the image, if one is present
824 	if (imageSize > 0)
825 	{
826 		calculatedHeight = std::max<int>(calculatedHeight, imageSize + (WZ_NOTIFICATION_PADDING * 2));
827 	}
828 	psNewNotificationForm->setGeometry(psNewNotificationForm->x(), psNewNotificationForm->y(), psNewNotificationForm->width(), calculatedHeight);
829 
830 	return psNewNotificationForm;
831 }
832 
~W_NOTIFICATION()833 W_NOTIFICATION::~W_NOTIFICATION()
834 {
835 	if (pImageTexture)
836 	{
837 		delete pImageTexture;
838 		pImageTexture = nullptr;
839 	}
840 }
841 
842 #include "lib/ivis_opengl/piestate.h"
843 
makeTexture(unsigned int width,unsigned int height,const gfx_api::pixel_format & format,const void * image)844 gfx_api::texture* makeTexture(unsigned int width, unsigned int height, const gfx_api::pixel_format& format, const void *image)
845 {
846 	size_t mip_count = floor(log(std::max(width, height))) + 1;
847 	gfx_api::texture* mTexture = gfx_api::context::get().create_texture(mip_count, width, height, format);
848 	if (image != nullptr)
849 		mTexture->upload_and_generate_mipmaps(0u, 0u, width, height, format, image);
850 
851 	return mTexture;
852 }
853 
loadImage(const WZ_Notification_Image & image)854 gfx_api::texture* W_NOTIFICATION::loadImage(const WZ_Notification_Image& image)
855 {
856 	gfx_api::texture* pTexture = nullptr;
857 	iV_Image ivImage;
858 	if (!image.imagePath().empty())
859 	{
860 		const std::string& filename = image.imagePath();
861 		const char *extension = strrchr(filename.c_str(), '.'); // determine the filetype
862 
863 		if (!extension || strcmp(extension, ".png") != 0)
864 		{
865 			debug(LOG_ERROR, "Bad image filename: %s", filename.c_str());
866 			return nullptr;
867 		}
868 		if (!iV_loadImage_PNG(filename.c_str(), &ivImage))
869 		{
870 			return nullptr;
871 		}
872 	}
873 	else if (!image.memoryBuffer().empty())
874 	{
875 		auto result = iV_loadImage_PNG(image.memoryBuffer(), &ivImage);
876 		if (!result.noError())
877 		{
878 			debug(LOG_ERROR, "Failed to load image from memory buffer: %s", result.text.c_str());
879 			return nullptr;
880 		}
881 	}
882 	else
883 	{
884 		// empty or unhandled case
885 		return nullptr;
886 	}
887 	pTexture = makeTexture(ivImage.width, ivImage.height, iV_getPixelFormat(&ivImage), ivImage.bmp);
888 	iV_unloadImage(&ivImage);
889 	return pTexture;
890 }
891 
892 #ifndef M_PI_2
893 # define M_PI_2		1.57079632679489661923132169163975144	/* pi/2 */
894 #endif
895 
EaseOutElastic(float p)896 float EaseOutElastic(float p)
897 {
898 	return sin(-13 * M_PI_2 * (p + 1)) * pow(2, -10 * p) + 1;
899 }
900 
EaseOutQuint(float p)901 float EaseOutQuint(float p)
902 {
903 	float f = (p - 1);
904 	return f * f * f * f * f + 1;
905 }
906 
EaseInCubic(float p)907 float EaseInCubic(float p)
908 {
909 	return p * p * p;
910 }
911 
912 #define WZ_NOTIFICATION_OPEN_DURATION (GAME_TICKS_PER_SEC*1) // Time duration for notification open animation, in game ticks
913 #define WZ_NOTIFICATION_CLOSE_DURATION (WZ_NOTIFICATION_OPEN_DURATION / 2)
914 #define WZ_NOTIFICATION_TOP_PADDING 5
915 #define WZ_NOTIFICATION_MIN_DELAY_BETWEEN (GAME_TICKS_PER_SEC*1) // Minimum delay between display of notifications, in game ticks
916 
calculateNotificationWidgetPos()917 bool W_NOTIFICATION::calculateNotificationWidgetPos()
918 {
919 	// center horizontally in window
920 	int x = std::max<int>((screenWidth - width()) / 2, 0);
921 	int y = 0; // set below
922 	const int endingYPosition = WZ_NOTIFICATION_TOP_PADDING;
923 
924 	// calculate positioning based on state and time
925 	switch (request->status.state)
926 	{
927 		case WZ_Notification_Status::NotificationState::waiting:
928 			// first chance to display
929 			request->setState(WZ_Notification_Status::NotificationState::opening);
930 			// fallthrough
931 		case WZ_Notification_Status::NotificationState::opening:
932 		{
933 			// calculate opening progress based on the stateStartTime
934 			const uint32_t startTime = request->status.stateStartTime;
935 			float openAnimationDuration = float(WZ_NOTIFICATION_OPEN_DURATION) * request->status.animationSpeed;
936 			uint32_t endTime = startTime + uint32_t(openAnimationDuration);
937 			if (realTime < endTime)
938 			{
939 				y = (-height()) + (EaseOutQuint((float(realTime) - float(startTime)) / float(openAnimationDuration)) * (endingYPosition + height())) + 1;
940 				if (!(y + getDragOffset().y >= endingYPosition))
941 				{
942 					break;
943 				}
944 				else
945 				{
946 					// factoring in the drag, the notification is already at (or past) fully open
947 					// so fall through to immediately transition to the "shown" state
948 				}
949 			}
950 			request->setState(WZ_Notification_Status::NotificationState::shown);
951 		} // fallthrough
952 		case WZ_Notification_Status::NotificationState::shown:
953 		{
954 			const auto& duration = request->notification.duration;
955 			if (duration > 0 && !isActivelyBeingDragged())
956 			{
957 				if (getDragOffset().y > 0)
958 				{
959 					// when dragging a notification more *open* (/down),
960 					// ensure that the notification remains displayed for at least one additional second
961 					// beyond the bounce-back from the drag release
962 					request->status.stateStartTime = std::max<uint32_t>(request->status.stateStartTime, realTime - duration + GAME_TICKS_PER_SEC);
963 				}
964 			}
965 			if (duration == 0 || (realTime < (request->status.stateStartTime + duration)) || isActivelyBeingDragged())
966 			{
967 				y = endingYPosition;
968 				if ((y + getDragOffset().y) > ((-height() / 3) * 2))
969 				{
970 					break;
971 				}
972 			}
973 			request->setState(WZ_Notification_Status::NotificationState::closing);
974 		} // fallthrough
975 		case WZ_Notification_Status::NotificationState::closing:
976 		{
977 			// calculate closing progress based on the stateStartTime
978 			const uint32_t startTime = request->status.stateStartTime;
979 			float closeAnimationDuration = float(WZ_NOTIFICATION_CLOSE_DURATION) * request->status.animationSpeed;
980 			uint32_t endTime = startTime + uint32_t(closeAnimationDuration);
981 			if (realTime < endTime)
982 			{
983 				float percentComplete = (float(realTime) - float(startTime)) / float(closeAnimationDuration);
984 				if (getDragOffset().y >= 0)
985 				{
986 					percentComplete = EaseInCubic(percentComplete);
987 				}
988 				y = endingYPosition - (percentComplete * (endingYPosition + height()));
989 				if ((y + getDragOffset().y) > -height())
990 				{
991 					break;
992 				}
993 				else
994 				{
995 					// closed early (because of drag offset)
996 					// drop through and signal "closed" state
997 				}
998 			}
999 			request->setState(WZ_Notification_Status::NotificationState::closed);
1000 
1001 		} // fallthrough
1002 		case WZ_Notification_Status::NotificationState::closed:
1003 			// widget is now off-screen - get checkbox state (if present)
1004 			bool bDoNotShowAgain = false;
1005 			if (pOnDoNotShowAgainCheckbox)
1006 			{
1007 				bDoNotShowAgain = pOnDoNotShowAgainCheckbox->getIsChecked();
1008 			}
1009 			// then destroy the widget, and finalize the notification request
1010 			removeInGameNotificationForm(request);
1011 			finishedProcessingNotificationRequest(request, bDoNotShowAgain); // after this, request is invalid!
1012 			request = nullptr; // TEMP
1013 			return true; // processed notification
1014 	}
1015 
1016 	x += getDragOffset().x;
1017 	y += getDragOffset().y;
1018 
1019 	move(x, y);
1020 
1021 	return false;
1022 }
1023 
1024 /* Run a notification widget */
run(W_CONTEXT * psContext)1025 void W_NOTIFICATION::run(W_CONTEXT *psContext)
1026 {
1027 	if (isInDragMode && mouseDown(MOUSE_LMB))
1028 	{
1029 		int dragStartY = dragStartMousePos.y;
1030 		int currMouseY = mouseY();
1031 
1032 		// calculate how much to respond to the drag by comparing the start to the current position
1033 		if (dragStartY > currMouseY)
1034 		{
1035 			// dragging up (to close) - respond 1 to 1
1036 			int distanceY = dragStartY - currMouseY;
1037 			dragOffset.y = (distanceY > 0) ? -(distanceY) : 0;
1038 //			debug(LOG_GUI, "dragging up, dragOffset.y: (%d)", dragOffset.y);
1039 		}
1040 		else if (currMouseY > dragStartY)
1041 		{
1042 			// dragging down
1043 			const int verticalLimit = 10;
1044 			int distanceY = currMouseY - dragStartY;
1045 			dragOffset.y = verticalLimit * (1 + log10(float(distanceY) / float(verticalLimit)));
1046 //			debug(LOG_GUI, "dragging down, dragOffset.y: (%d)", dragOffset.y);
1047 		}
1048 		else
1049 		{
1050 			dragOffset.y = 0;
1051 		}
1052 	}
1053 	else
1054 	{
1055 		if (isInDragMode && !mouseDown(MOUSE_LMB))
1056 		{
1057 //			debug(LOG_GUI, "No longer in drag mode");
1058 			isInDragMode = false;
1059 			dragEndedTime = realTime;
1060 			dragOffsetEnded = dragOffset;
1061 			notificationDidStopDragOnNotification();
1062 		}
1063 		if (request->status.state != WZ_Notification_Status::NotificationState::closing)
1064 		{
1065 			// decay drag offset
1066 			const uint32_t dragDecayDuration = GAME_TICKS_PER_SEC * 1;
1067 			if (dragOffset.y != 0)
1068 			{
1069 				dragOffset.y = dragOffsetEnded.y - (int)(float(dragOffsetEnded.y) * EaseOutElastic((float(realTime) - float(dragEndedTime)) / float(dragDecayDuration)));
1070 			}
1071 		}
1072 	}
1073 
1074 	calculateNotificationWidgetPos();
1075 }
1076 
clicked(W_CONTEXT * psContext,WIDGET_KEY key)1077 void W_NOTIFICATION::clicked(W_CONTEXT *psContext, WIDGET_KEY key)
1078 {
1079 	if (request->status.state == WZ_Notification_Status::NotificationState::closing)
1080 	{
1081 		// if clicked while closing, set state to shown
1082 //		debug(LOG_GUI, "Click while closing - set to shown");
1083 		request->status.state = WZ_Notification_Status::NotificationState::shown;
1084 		request->status.stateStartTime = realTime;
1085 	}
1086 
1087 	if (geometry().contains(psContext->mx, psContext->my))
1088 	{
1089 //		debug(LOG_GUI, "Enabling drag mode");
1090 		isInDragMode = true;
1091 		dragStartMousePos.x = psContext->mx;
1092 		dragStartMousePos.y = psContext->my;
1093 //		debug(LOG_GUI, "dragStartMousePos: (%d x %d)", dragStartMousePos.x, dragStartMousePos.y);
1094 		dragStartedTime = realTime;
1095 		notificationsDidStartDragOnNotification(dragStartMousePos);
1096 	}
1097 }
1098 
1099 #define WZ_NOTIFICATION_DOWN_DRAG_DISCARD_CLICK_THRESHOLD	5
1100 
released(W_CONTEXT * psContext,WIDGET_KEY key)1101 void W_NOTIFICATION::released(W_CONTEXT *psContext, WIDGET_KEY key)
1102 {
1103 //	debug(LOG_GUI, "released");
1104 
1105 	if (request)
1106 	{
1107 		if (isInDragMode && dragOffset.y < WZ_NOTIFICATION_DOWN_DRAG_DISCARD_CLICK_THRESHOLD)
1108 		{
1109 			internalDismissNotification();
1110 		}
1111 	}
1112 }
1113 
display(int xOffset,int yOffset)1114 void W_NOTIFICATION::display(int xOffset, int yOffset)
1115 {
1116 	int x0 = x() + xOffset;
1117 	int y0 = y() + yOffset;
1118 	int x1 = x0 + width();
1119 	int y1 = y0 + height();
1120 	// Need to do a little trick here - ensure the bounds are always positive, adjust the height
1121 	if (y() < 0)
1122 	{
1123 		y0 += -y();
1124 	}
1125 
1126 	pie_UniTransBoxFill(x0, y0, x1, y1, pal_RGBA(255, 255, 255, 50));
1127 	pie_UniTransBoxFill(x0, y0, x1, y1, pal_RGBA(0, 0, 0, 50));
1128 
1129 	pie_UniTransBoxFill(x0, y0, x1, y1, WZCOL_NOTIFICATION_BOX);
1130 	iV_Box2(x0, y0, x1, y1, WZCOL_FORM_DARK, WZCOL_FORM_DARK);
1131 	iV_Box2(x0 - 1, y0 - 1, x1 + 1, y1 + 1, pal_RGBA(255, 255, 255, 50), pal_RGBA(255, 255, 255, 50));
1132 
1133 	// Display the image, if present
1134 	int imageLeft = x1 - WZ_NOTIFICATION_PADDING - WZ_NOTIFICATION_IMAGE_SIZE;
1135 	int imageTop = (y() + yOffset) + WZ_NOTIFICATION_PADDING;
1136 
1137 	if (pImageTexture)
1138 	{
1139 		int image_x0 = imageLeft;
1140 		int image_y0 = imageTop;
1141 
1142 		iV_DrawImageAnisotropic(*pImageTexture, Vector2i(image_x0, image_y0), Vector2f(0,0), Vector2f(WZ_NOTIFICATION_IMAGE_SIZE, WZ_NOTIFICATION_IMAGE_SIZE), 0.f, WZCOL_WHITE);
1143 	}
1144 }
1145 
1146 // MARK: - In-Game Notification System Functions
1147 
notificationsDidStartDragOnNotification(const Vector2i & dragStartPos)1148 void notificationsDidStartDragOnNotification(const Vector2i& dragStartPos)
1149 {
1150 	lastDragOnNotificationStartPos = dragStartPos;
1151 }
1152 
notificationDidStopDragOnNotification()1153 void notificationDidStopDragOnNotification()
1154 {
1155 	lastDragOnNotificationStartPos = Vector2i(-1, -1);
1156 }
1157 
notificationsInitialize()1158 bool notificationsInitialize()
1159 {
1160 	notificationPrefs = new WZ_Notification_Preferences("notifications.json");
1161 
1162 	// Initialize the notifications overlay screen
1163 	psNotificationOverlayScreen = W_SCREEN::make();
1164 	psNotificationOverlayScreen->psForm->hide(); // hiding the root form does not stop display of children, but *does* prevent it from accepting mouse over itself - i.e. basically makes it transparent
1165 	widgRegisterOverlayScreen(psNotificationOverlayScreen, std::numeric_limits<uint16_t>::max());
1166 
1167 	return true;
1168 }
1169 
notificationsShutDown()1170 void notificationsShutDown()
1171 {
1172 	if (notificationPrefs)
1173 	{
1174 		notificationPrefs->savePreferences();
1175 		delete notificationPrefs;
1176 		notificationPrefs = nullptr;
1177 	}
1178 
1179 	if (currentInGameNotification)
1180 	{
1181 		widgDelete(currentInGameNotification.get());
1182 		currentInGameNotification = nullptr;
1183 	}
1184 
1185 	if (psNotificationOverlayScreen)
1186 	{
1187 		widgRemoveOverlayScreen(psNotificationOverlayScreen);
1188 		psNotificationOverlayScreen = nullptr;
1189 	}
1190 }
1191 
isDraggingInGameNotification()1192 bool isDraggingInGameNotification()
1193 {
1194 	// right now we only support a single concurrent notification
1195 	if (currentInGameNotification)
1196 	{
1197 		if (currentInGameNotification->isActivelyBeingDragged())
1198 		{
1199 			return true;
1200 		}
1201 	}
1202 
1203 	// also handle the case where a drag starts, the notification dismisses itself, but the user has not released the mouse button
1204 	// in this case, we want to consider it an in-game notification drag *until* the mouse button is released
1205 
1206 	return (lastDragOnNotificationStartPos.x >= 0 && lastDragOnNotificationStartPos.y >= 0);
1207 }
1208 
getOrCreateInGameNotificationForm(WZ_Queued_Notification * request)1209 std::shared_ptr<W_NOTIFICATION> getOrCreateInGameNotificationForm(WZ_Queued_Notification* request)
1210 {
1211 	if (!request) return nullptr;
1212 
1213 	// right now we only support a single concurrent notification
1214 	if (!currentInGameNotification)
1215 	{
1216 		currentInGameNotification = W_NOTIFICATION::make(request);
1217 	}
1218 	return currentInGameNotification;
1219 }
1220 
1221 
displayNotificationInGame(WZ_Queued_Notification * request)1222 void displayNotificationInGame(WZ_Queued_Notification* request)
1223 {
1224 	ASSERT(request, "request is null");
1225 	getOrCreateInGameNotificationForm(request);
1226 	// NOTE: Can ignore the result of the above, because it automatically attaches it to the root notification overlay screen
1227 }
1228 
displayNotification(WZ_Queued_Notification * request)1229 void displayNotification(WZ_Queued_Notification* request)
1230 {
1231 	ASSERT(request, "request is null");
1232 
1233 	// By default, display the notification using the in-game notification system
1234 	displayNotificationInGame(request);
1235 }
1236 
1237 // run in-game notifications queue
runNotifications()1238 void runNotifications()
1239 {
1240 	// at the moment, we only support displaying a single notification at a time
1241 
1242 	if (!currentNotification)
1243 	{
1244 		if ((realTime - lastNotificationClosed) < WZ_NOTIFICATION_MIN_DELAY_BETWEEN)
1245 		{
1246 			// wait to fetch a new notification till a future cycle
1247 			return;
1248 		}
1249 		// check for a new notification to display
1250 		currentNotification = popNextQueuedNotification();
1251 		if (currentNotification)
1252 		{
1253 			// display the new notification
1254 			displayNotification(currentNotification.get());
1255 		}
1256 	}
1257 
1258 	if (!currentInGameNotification || !currentInGameNotification->isActivelyBeingDragged())
1259 	{
1260 		if (lastDragOnNotificationStartPos.x >= 0 && lastDragOnNotificationStartPos.y >= 0)
1261 		{
1262 			UDWORD currDragStartX, currDragStartY;
1263 			if (mouseDrag(MOUSE_LMB, &currDragStartX, &currDragStartY))
1264 			{
1265 				if (currDragStartX != lastDragOnNotificationStartPos.x || currDragStartY != lastDragOnNotificationStartPos.y)
1266 				{
1267 					notificationDidStopDragOnNotification(); // ensure last notification drag position is cleared
1268 				}
1269 			}
1270 			else
1271 			{
1272 				notificationDidStopDragOnNotification(); // ensure last notification drag position is cleared
1273 			}
1274 		}
1275 	}
1276 }
1277 
removeInGameNotificationForm(WZ_Queued_Notification * request)1278 void removeInGameNotificationForm(WZ_Queued_Notification* request)
1279 {
1280 	if (!request) return;
1281 
1282 	// right now we only support a single concurrent notification
1283 	currentInGameNotification->deleteLater();
1284 	currentInGameNotification = nullptr;
1285 }
1286 
finishedProcessingNotificationRequest(WZ_Queued_Notification * request,bool doNotShowAgain)1287 void finishedProcessingNotificationRequest(WZ_Queued_Notification* request, bool doNotShowAgain)
1288 {
1289 	// at the moment, we only support processing a single notification at a time
1290 
1291 	if (doNotShowAgain && !request->wasProgrammaticallyDismissed())
1292 	{
1293 		ASSERT(!request->notification.displayOptions.uniqueNotificationIdentifier().empty(), "Do Not Show Again was selected, but notification has no ignore key");
1294 		debug(LOG_GUI, "Do Not Show Notification Again: %s", request->notification.displayOptions.uniqueNotificationIdentifier().c_str());
1295 		notificationPrefs->doNotShowNotificationAgain(request->notification.displayOptions.uniqueNotificationIdentifier());
1296 	}
1297 
1298 	if (request->notification.onDismissed)
1299 	{
1300 		request->notification.onDismissed(request->notification, request->dismissReason());
1301 	}
1302 
1303 	// finished with this notification
1304 	currentNotification.reset(); // at this point request is no longer valid!
1305 	lastNotificationClosed = realTime;
1306 }
1307 
1308 // MARK: - Base functions for handling Notifications
1309 
addNotification(const WZ_Notification & notification,const WZ_Notification_Trigger & trigger)1310 void addNotification(const WZ_Notification& notification, const WZ_Notification_Trigger& trigger)
1311 {
1312 	// Verify notification properties
1313 	if (notification.contentTitle.empty())
1314 	{
1315 		debug(LOG_ERROR, "addNotification called with empty notification.contentTitle");
1316 		return;
1317 	}
1318 	if (notification.contentText.empty())
1319 	{
1320 		debug(LOG_ERROR, "addNotification called with empty notification.contentText");
1321 		return;
1322 	}
1323 
1324 	// Add the notification to the notification system's queue
1325 	notificationQueue.push_back(std::unique_ptr<WZ_Queued_Notification>(new WZ_Queued_Notification(notification, WZ_Notification_Status(realTime), trigger)));
1326 }
1327 
removeNotificationPreferencesIf(const std::function<bool (const std::string & uniqueNotificationIdentifier)> & matchIdentifierFunc)1328 bool removeNotificationPreferencesIf(const std::function<bool (const std::string& uniqueNotificationIdentifier)>& matchIdentifierFunc)
1329 {
1330 	ASSERT_OR_RETURN(false, notificationPrefs, "notificationPrefs is null");
1331 	return notificationPrefs->removeNotificationPreferencesIf(matchIdentifierFunc);
1332 }
1333 
1334 // Whether one or more notifications with the specified tag (exact match) are currently-displayed or queued
1335 // If `scope` is `DISPLAYED_ONLY`, only currently-displayed notifications will be processed
1336 // If `scope` is `QUEUED_ONLY`, only queued notifications will be processed
hasNotificationsWithTag(const std::string & tag,NotificationScope scope)1337 bool hasNotificationsWithTag(const std::string& tag, NotificationScope scope /*= NotificationScope::DISPLAYED_AND_QUEUED*/)
1338 {
1339 	if ((scope == NotificationScope::DISPLAYED_AND_QUEUED || scope == NotificationScope::DISPLAYED_ONLY) && currentInGameNotification)
1340 	{
1341 		if (currentInGameNotification->notificationTag() == tag)
1342 		{
1343 			return true;
1344 		}
1345 	}
1346 
1347 	if ((scope == NotificationScope::DISPLAYED_AND_QUEUED || scope == NotificationScope::QUEUED_ONLY))
1348 	{
1349 		for (auto& queuedNotification : notificationQueue)
1350 		{
1351 			if (queuedNotification->notification.tag == tag)
1352 			{
1353 				return true;
1354 			}
1355 		}
1356 	}
1357 	return false;
1358 }
1359 
1360 // Cancel or dismiss existing notifications by tag (exact match)
1361 // If `scope` is `DISPLAYED_ONLY`, only currently-displayed notifications will be processed
1362 // If `scope` is `QUEUED_ONLY`, only queued notifications will be processed
1363 //
1364 // Returns: `true` if one or more notifications were cancelled or dismissed
cancelOrDismissNotificationsWithTag(const std::string & desiredTag,NotificationScope scope)1365 bool cancelOrDismissNotificationsWithTag(const std::string& desiredTag, NotificationScope scope /*= NotificationScope::DISPLAYED_AND_QUEUED*/)
1366 {
1367 	return cancelOrDismissNotificationIfTag([desiredTag](const std::string& tag) {
1368 		return desiredTag == tag;
1369 	}, scope);
1370 }
1371 
1372 // Cancel or dismiss existing notifications by tag
1373 // Accepts a `matchTagFunc` that receives each (queued / currently-displayed) notification's tag,
1374 // and returns "true" if it should be cancelled or dismissed (if queued / currently-displayed)
1375 // If `scope` is `DISPLAYED_ONLY`, only currently-displayed notifications will be processed
1376 // If `scope` is `QUEUED_ONLY`, only queued notifications will be processed
1377 //
1378 // Returns: `true` if one or more notifications were cancelled or dismissed
cancelOrDismissNotificationIfTag(const std::function<bool (const std::string & tag)> & matchTagFunc,NotificationScope scope)1379 bool cancelOrDismissNotificationIfTag(const std::function<bool (const std::string& tag)>& matchTagFunc, NotificationScope scope /*= NotificationScope::DISPLAYED_AND_QUEUED*/)
1380 {
1381 	size_t numCancelledNotifications = 0;
1382 
1383 	if ((scope == NotificationScope::DISPLAYED_AND_QUEUED || scope == NotificationScope::DISPLAYED_ONLY) && currentInGameNotification)
1384 	{
1385 		if (matchTagFunc(currentInGameNotification->notificationTag()))
1386 		{
1387 			if (currentInGameNotification->dismissNotification())
1388 			{
1389 				debug(LOG_GUI, "Dismissing currently-displayed notification with tag: [%s]", currentInGameNotification->notificationTag().c_str());
1390 				++numCancelledNotifications;
1391 			}
1392 		}
1393 	}
1394 
1395 	if ((scope == NotificationScope::DISPLAYED_AND_QUEUED || scope == NotificationScope::QUEUED_ONLY))
1396 	{
1397 		auto it = notificationQueue.begin();
1398 		while (it != notificationQueue.end())
1399 		{
1400 			auto & request = *it;
1401 
1402 			if (matchTagFunc(request->notification.tag))
1403 			{
1404 				// cancel this notification - remove from the queue
1405 				debug(LOG_GUI, "Cancelling queued notification: [%s]; with tag: [%s]", request->notification.contentTitle.c_str(), request->notification.tag.c_str());
1406 				it = notificationQueue.erase(it);
1407 				++numCancelledNotifications;
1408 				continue;
1409 			}
1410 
1411 			++it;
1412 		}
1413 	}
1414 
1415 	return numCancelledNotifications > 0;
1416 }
1417