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