overte/scripts/system/notifications.js
Alezia Kurdis 4a49631007
Added HMD notifications dismissal
Added a gestural way to dismiss the notifications in HMD.

The notifications vanishes after 10 sec. 
But if for any reason we want to accelerate the process (mostly because it hide the view or it is going to appears in photo capture)
In Desktop we can simply click on the notification to get rid of them.
But in HMD, clicking was kinda a pain (assuming the if you want to dismiss the notification is often because they are already annoying you)
have to aim and click is like pressing a button using a fishing pole, it's certainly adding more annoyance to this.
To addressed that, I introduced the "Whoosh!": An easy gesture to dismiss any 3d UI, by simply move one of you controller over you eyes height. (a bit like making flee an annoying fly.)
2022-09-16 22:29:02 -04:00

430 lines
18 KiB
JavaScript

"use strict";
//
// notifications.js
//
// Created by Adrian McCarlie on October 8th, 2014
// Copyright 2014 High Fidelity, Inc.
// Copyright 2022 Overte e.V.
//
// Display notifications to the user for some specific events.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function () {
Script.include([
"create/audioFeedback/audioFeedback.js"
]);
var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications";
var SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS = "snapshotNotifications";
var NOTIFICATION_LIFE_DURATION = 10000; //10 seconds (in millisecond) before expiration.
var FADE_OUT_DURATION = 1000; //1 seconds (in millisecond) to fade out.
var NOTIFICATION_ALPHA = 0.9; // a value between: 0.0 (transparent) and 1.0 (fully opaque).
var MAX_LINE_LENGTH = 42;
var notifications = [];
var newEventDetected = false;
var isOnHMD = HMD.active;
var textColor = { "red": 228, "green": 228, "blue": 228};
var backColor = { "red": 2, "green": 2, "blue": 2};
//DESKTOP OVERLAY PROPERTIES
var overlayWidth = 340.0; //width in pixel of notification overlay in desktop
var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window
var overlayLocationX = (windowDimensions.x - (overlayWidth + 20.0)); // positions window 20px from the right of the interface window
var overlayLocationY = 20.0; // position down from top of interface window
var overlayTopMargin = 13.0;
var overlayLeftMargin = 10.0;
var overlayFontSize = 12.0;
var TEXT_OVERLAY_FONT_SIZE_IN_PIXELS = 18.0; // taken from TextOverlay::textSize
var DESKTOP_INTER_SPACE_NOTIFICATION = 5; //5 px
//HMD NOTIFICATION PANEL PROPERTIES
var HMD_UI_SCALE_FACTOR = 1.0; //This define the size of all the notification system in HMD.
var hmdPanelLocalPosition = {"x": 1.2, "y": 2, "z": -1.0};
var hmdPanelLocalRotation = Quat.fromVec3Degrees({"x": 0, "y": -15, "z": 0});
var mainHMDnotificationContainerID = Uuid.NULL;
//HMD LOCAL ENTITY PROPERTIES
var entityWidth = 0.8; //in meter
var HMD_LINE_HEIGHT = 0.03;
var entityTopMargin = 0.02;
var entityLeftMargin = 0.02;
var HMD_INTER_SPACE_NOTIFICATION = 0.05;
//ACTIONS
// handles clicks on notifications overlays to delete notifications. (DESKTOP only)
function mousePressEvent(event) {
if (!isOnHMD) {
var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y });
for (var i = 0; i < notifications.length; i += 1) {
if (clickedOverlay === notifications[i].overlayID || clickedOverlay === notifications[i].imageOverlayID) {
deleteSpecificNotification(i);
notifications.splice(i, 1);
newEventDetected = true;
}
}
}
}
function checkHands() {
var myLeftHand = Controller.getPoseValue(Controller.Standard.LeftHand);
var myRightHand = Controller.getPoseValue(Controller.Standard.RightHand);
var eyesPosition = MyAvatar.getEyePosition();
var hipsPosition = MyAvatar.getJointPosition("Hips");
var eyesRelativeHeight = eyesPosition.y - hipsPosition.y;
if (myLeftHand.translation.y > eyesRelativeHeight || myRightHand.translation.y > eyesRelativeHeight) {
deleteAllExistingNotificationsDisplayed();
notifications = [];
audioFeedback.action();
}
}
//DISPLAY
function renderNotifications(remainingTime) {
var alpha = NOTIFICATION_ALPHA;
if (remainingTime < FADE_OUT_DURATION) {
alpha = NOTIFICATION_ALPHA * (remainingTime/FADE_OUT_DURATION);
}
var properties, count, extraline, breaks, height;
var breakPoint = MAX_LINE_LENGTH + 1;
var level = overlayLocationY;
var entityLevel = 0;
if (notifications.length > 0) {
for (var i = 0; i < notifications.length; i++) {
count = (notifications[i].dataText.match(/\n/g) || []).length,
extraLine = 0;
breaks = 0;
if (notifications[i].dataText.length >= breakPoint) {
breaks = count;
}
if (isOnHMD) {
//use HMD local entities
var sensorScaleFactor = MyAvatar.sensorToWorldScale * HMD_UI_SCALE_FACTOR;
var lineHeight = HMD_LINE_HEIGHT;
height = lineHeight + (2 * entityTopMargin);
extraLine = breaks * lineHeight;
height = (height + extraLine) * HMD_UI_SCALE_FACTOR;
entityLevel = entityLevel - (height/2);
properties = {
"type": "Text",
"parentID": mainHMDnotificationContainerID,
"localPosition": {"x": 0, "y": entityLevel, "z": 0},
"dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01},
"isVisibleInSecondaryCamera": false,
"lineHeight": lineHeight * sensorScaleFactor,
"textColor": textColor,
"textAlpha": alpha,
"backgroundColor": backColor,
"backgroundAlpha": alpha,
"leftMargin": entityLeftMargin * sensorScaleFactor,
"topMargin": entityTopMargin * sensorScaleFactor,
"unlit": true,
"renderLayer": "hud"
};
if (notifications[i].entityID === Uuid.NULL){
properties.text = notifications[i].dataText;
notifications[i].entityID = Entities.addEntity(properties, "local");
} else {
Entities.editEntity(notifications[i].entityID, properties);
}
if (notifications[i].dataImage !== null) {
entityLevel = entityLevel - (height/2);
height = (entityWidth / notifications[i].dataImage.aspectRatio) * HMD_UI_SCALE_FACTOR;
entityLevel = entityLevel - (height/2);
properties = {
"type": "Image",
"parentID": mainHMDnotificationContainerID,
"localPosition": {"x": 0, "y": entityLevel, "z": 0},
"dimensions": {"x": (entityWidth * HMD_UI_SCALE_FACTOR), "y": height, "z": 0.01},
"isVisibleInSecondaryCamera": false,
"emissive": true,
"visible": true,
"alpha": alpha,
"renderLayer": "hud"
};
if (notifications[i].imageEntityID === Uuid.NULL){
properties.imageURL = notifications[i].dataImage.path;
notifications[i].imageEntityID = Entities.addEntity(properties, "local");
} else {
Entities.editEntity(notifications[i].imageEntityID, properties);
}
}
entityLevel = entityLevel - (height/2) - (HMD_INTER_SPACE_NOTIFICATION * HMD_UI_SCALE_FACTOR);
} else {
//use Desktop overlays
height = 40.0;
extraLine = breaks * TEXT_OVERLAY_FONT_SIZE_IN_PIXELS;
height = height + extraLine;
properties = {
"x": overlayLocationX,
"y": level,
"width": overlayWidth,
"height": height,
"color": textColor,
"backgroundColor": backColor,
"alpha": alpha,
"topMargin": overlayTopMargin,
"leftMargin": overlayLeftMargin,
"font": {"size": overlayFontSize}
};
if (notifications[i].overlayID === Uuid.NULL){
properties.text = notifications[i].dataText;
notifications[i].overlayID = Overlays.addOverlay("text", properties);
} else {
Overlays.editOverlay(notifications[i].overlayID, properties);
}
if (notifications[i].dataImage !== null) {
level = level + height;
height = overlayWidth / notifications[i].dataImage.aspectRatio;
properties = {
"x": overlayLocationX,
"y": level,
"width": overlayWidth,
"height": height,
"subImage": { "x": 0, "y": 0 },
"visible": true,
"alpha": alpha
};
if (notifications[i].imageOverlayID === Uuid.NULL){
properties.imageURL = notifications[i].dataImage.path;
notifications[i].imageOverlayID = Overlays.addOverlay("image", properties);
} else {
Overlays.editOverlay(notifications[i].imageOverlayID, properties);
}
}
level = level + height + DESKTOP_INTER_SPACE_NOTIFICATION;
}
}
}
}
function deleteAllExistingNotificationsDisplayed() {
if (notifications.length > 0) {
for (var i = 0; i < notifications.length; i++) {
deleteSpecificNotification(i);
}
}
}
function deleteSpecificNotification(indexNotification) {
if (notifications[indexNotification].entityID !== Uuid.NULL){
Entities.deleteEntity(notifications[indexNotification].entityID);
notifications[indexNotification].entityID = Uuid.NULL;
}
if (notifications[indexNotification].overlayID !== Uuid.NULL){
Overlays.deleteOverlay(notifications[indexNotification].overlayID);
notifications[indexNotification].overlayID = Uuid.NULL;
}
if (notifications[indexNotification].imageEntityID !== Uuid.NULL){
Entities.deleteEntity(notifications[indexNotification].imageEntityID);
notifications[indexNotification].imageEntityID = Uuid.NULL;
}
if (notifications[indexNotification].imageOverlayID !== Uuid.NULL){
Overlays.deleteOverlay(notifications[indexNotification].imageOverlayID);
notifications[indexNotification].imageOverlayID = Uuid.NULL;
}
}
function createMainHMDnotificationContainer() {
if (mainHMDnotificationContainerID === Uuid.NULL) {
var properties = {
"type": "Shape",
"shape": "Cube",
"visible": false,
"dimensions": {"x": 0.1, "y": 0.1, "z":0.1},
"parentID": MyAvatar.sessionUUID,
"parentJointIndex": -2,
"localPosition": hmdPanelLocalPosition,
"localRotation": hmdPanelLocalRotation
};
mainHMDnotificationContainerID = Entities.addEntity(properties, "local");
}
}
function deleteMainHMDnotificationContainer() {
if (mainHMDnotificationContainerID !== Uuid.NULL) {
Entities.deleteEntity(mainHMDnotificationContainerID);
mainHMDnotificationContainerID = Uuid.NULL;
}
}
//UTILITY FUNCTIONS
// Trims extra whitespace and breaks into lines of length no more
// than MAX_LINE_LENGTH, breaking at spaces. Trims extra whitespace.
function wordWrap(string) {
var finishedLines = [], currentLine = '';
string.split(/\s/).forEach(function (word) {
var tail = currentLine ? ' ' + word : word;
if ((currentLine.length + tail.length) <= MAX_LINE_LENGTH) {
currentLine += tail;
} else {
finishedLines.push(currentLine);
currentLine = word;
if (currentLine.length > MAX_LINE_LENGTH) {
finishedLines.push(currentLine.substring(0,MAX_LINE_LENGTH));
currentLine = currentLine.substring(MAX_LINE_LENGTH, currentLine.length);
}
}
});
if (currentLine) {
finishedLines.push(currentLine);
}
return finishedLines.join('\n');
}
//NOTIFICATION STACK MANAGEMENT
function addNotification (dataText, dataImage) {
var d = new Date();
var notification = {
"dataText": dataText,
"dataImage": dataImage,
"timestamp": d.getTime(),
"entityID": Uuid.NULL,
"imageEntityID": Uuid.NULL,
"overlayID": Uuid.NULL,
"imageOverlayID": Uuid.NULL
};
notifications.push(notification);
newEventDetected = true;
if (notifications.length === 1) {
createMainHMDnotificationContainer();
Script.update.connect(update);
Controller.mousePressEvent.connect(mousePressEvent);
}
}
function update(deltaTime) {
if (notifications.length === 0 && !newEventDetected) {
Script.update.disconnect(update);
Controller.mousePressEvent.disconnect(mousePressEvent);
deleteMainHMDnotificationContainer();
} else {
if (isOnHMD !== HMD.active) {
deleteAllExistingNotificationsDisplayed();
isOnHMD = HMD.active;
}
var d = new Date();
var immediatly = d.getTime();
var mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION;
var expirationDetected = false;
for (var i = 0; i < notifications.length; i++) {
if ((immediatly - notifications[i].timestamp) > NOTIFICATION_LIFE_DURATION){
deleteSpecificNotification(i);
notifications.splice(i, 1);
expirationDetected = true;
} else {
mostRecentRemainingTime = NOTIFICATION_LIFE_DURATION - (immediatly - notifications[i].timestamp);
}
}
if (newEventDetected || expirationDetected || mostRecentRemainingTime < FADE_OUT_DURATION) {
renderNotifications(mostRecentRemainingTime);
newEventDetected = false;
}
}
if (isOnHMD) {
checkHands();
}
}
//NOTIFICATION EVENTS FUNCTIONS
function onDomainConnectionRefused(reason, reasonCode) {
// the "login error" reason means that the DS couldn't decrypt the username signature
// since this eventually resolves itself for good actors we don't need to show a notification for it
var LoginErrorMetaverse_REASON_CODE = 2;
if (reasonCode !== LoginErrorMetaverse_REASON_CODE) {
addNotification("Connection refused: " + reason, null);
}
}
function onEditError(msg) {
addNotification(wordWrap(msg), null);
}
function onNotify(msg) {
// Generic notification system for user feedback, thus using this
addNotification(wordWrap(msg), null);
}
function onMessageReceived(channel, message) {
if (channel === NOTIFICATIONS_MESSAGE_CHANNEL) {
message = JSON.parse(message);
addNotification(wordWrap(message.message), null);
}
}
function onSnapshotTaken(pathStillSnapshot, notify) {
if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) {
if (notify) {
var imageProperties = {
"path": "file:///" + pathStillSnapshot,
"aspectRatio": Window.innerWidth / Window.innerHeight
};
addNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), imageProperties);
}
}
}
function tabletNotification() {
addNotification("Tablet needs your attention", null);
}
function processingGif() {
if (Settings.getValue(SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS, true)) {
addNotification("Processing GIF snapshot...", null);
}
}
function connectionAdded(connectionName) {
addNotification(connectionName, null);
}
function connectionError(error) {
addNotification(wordWrap("Error trying to make connection: " + error), null);
}
//STARTING AND ENDING
function scriptEnding() {
//cleanup
deleteAllExistingNotificationsDisplayed();
//disconnecting
if (notifications.length > 0) {
Script.update.disconnect(update);
Controller.mousePressEvent.disconnect(mousePressEvent);
}
Script.scriptEnding.disconnect(scriptEnding);
Messages.unsubscribe(NOTIFICATIONS_MESSAGE_CHANNEL);
Window.domainConnectionRefused.disconnect(onDomainConnectionRefused);
Window.stillSnapshotTaken.disconnect(onSnapshotTaken);
Window.snapshot360Taken.disconnect(onSnapshotTaken);
Window.processingGifStarted.disconnect(processingGif);
Window.connectionAdded.disconnect(connectionAdded);
Window.connectionError.disconnect(connectionError);
Window.announcement.disconnect(onNotify);
Tablet.tabletNotification.disconnect(tabletNotification);
Messages.messageReceived.disconnect(onMessageReceived);
}
Script.scriptEnding.connect(scriptEnding);
//EVENTS TO NOTIFY
Window.domainConnectionRefused.connect(onDomainConnectionRefused);
Window.stillSnapshotTaken.connect(onSnapshotTaken);
Window.snapshot360Taken.connect(onSnapshotTaken);
Window.processingGifStarted.connect(processingGif);
Window.connectionAdded.connect(connectionAdded);
Window.connectionError.connect(connectionError);
Window.announcement.connect(onNotify);
Window.notifyEditError = onEditError;
Window.notify = onNotify;
Tablet.tabletNotification.connect(tabletNotification);
Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL);
Messages.messageReceived.connect(onMessageReceived);
}());