diff --git a/interface/resources/icons/tablet-icons/people-a-msg.svg b/interface/resources/icons/tablet-icons/people-a-msg.svg new file mode 100644 index 0000000000..862ce936ce --- /dev/null +++ b/interface/resources/icons/tablet-icons/people-a-msg.svg @@ -0,0 +1,83 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/people-i-msg.svg b/interface/resources/icons/tablet-icons/people-i-msg.svg new file mode 100644 index 0000000000..635a01be4b --- /dev/null +++ b/interface/resources/icons/tablet-icons/people-i-msg.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 35a0078d32..e8fc41da63 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -271,6 +271,8 @@ Rectangle { connectionsUserModel.getFirstPage(); } activeTab = "connectionsTab"; + connectionsOnlineDot.visible = false; + pal.sendToScript({method: 'hideNotificationDot'}); connectionsHelpText.color = hifi.colors.blueAccent; } } @@ -298,6 +300,16 @@ Rectangle { } } } + Rectangle { + id: connectionsOnlineDot; + visible: false; + width: 10; + height: width; + radius: width; + color: "#EF3B4E" + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; + } // "CONNECTIONS" text RalewaySemiBold { id: connectionsTabSelectorText; @@ -305,7 +317,11 @@ Rectangle { // Text size size: hifi.fontSizes.tabularData; // Anchors - anchors.fill: parent; + anchors.left: connectionsOnlineDot.visible ? connectionsOnlineDot.right : parent.left; + anchors.leftMargin: connectionsOnlineDot.visible ? 4 : 0; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; // Style font.capitalization: Font.AllUppercase; color: activeTab === "connectionsTab" ? hifi.colors.blueAccent : hifi.colors.baseGray; @@ -326,7 +342,7 @@ Rectangle { anchors.left: connectionsTabSelectorTextContainer.left; anchors.top: connectionsTabSelectorTextContainer.top; anchors.topMargin: 1; - anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42; + anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42 + connectionsOnlineDot.width + connectionsTabSelectorText.anchors.leftMargin; RalewayRegular { id: connectionsHelpText; text: "[?]"; @@ -1267,6 +1283,9 @@ Rectangle { case 'http.response': http.handleHttpResponse(message); break; + case 'changeConnectionsDotStatus': + connectionsOnlineDot.visible = message.shouldShowDot; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 65d98af234..47b9d354d0 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -408,9 +408,7 @@ Rectangle { Connections { onSendSignalToWallet: { - if (msg.method === 'walletReset' || msg.method === 'passphraseReset') { - sendToScript(msg); - } else if (msg.method === 'walletSecurity_changeSecurityImage') { + if (msg.method === 'walletSecurity_changeSecurityImage') { securityImageChange.initModel(); root.activeView = "securityImageChange"; } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index ba9a1f9fc9..5de8bb1a2a 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -252,12 +252,6 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; - Setting::Handle _settingSwitch{ "commerce", true }; - if (_settingSwitch.get()) { - openInspectionCertificate(); - } else { - openMarketplace(); - } emit contextOverlayClicked(_currentEntityWithContextOverlay); _contextOverlayJustClicked = true; } @@ -390,34 +384,6 @@ void ContextOverlayInterface::requestOwnershipVerification(const QUuid& entityID } } -static const QString INSPECTION_CERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; -void ContextOverlayInterface::openInspectionCertificate() { - // lets open the tablet to the inspection certificate QML - if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { - setLastInspectedEntity(_currentEntityWithContextOverlay); - auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - tablet->loadQMLSource(INSPECTION_CERTIFICATE_QML_PATH); - _hmdScriptingInterface->openTablet(); - } -} - -static const QString MARKETPLACE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; - -void ContextOverlayInterface::openMarketplace() { - // lets open the tablet and go to the current item in - // the marketplace (if the current entity has a - // marketplaceID) - if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { - auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - // construct the url to the marketplace item - QString url = MARKETPLACE_BASE_URL + _entityMarketplaceID; - QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; - tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); - _hmdScriptingInterface->openTablet(); - _isInMarketplaceInspectionMode = true; - } -} - void ContextOverlayInterface::enableEntityHighlight(const EntityItemID& entityItemID) { _selectionScriptingInterface->addToSelectedItemsList("contextOverlayHighlightList", "entity", entityItemID); } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index 808c3a4ee3..48b14e1a91 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -94,8 +94,6 @@ private: bool _isInMarketplaceInspectionMode { false }; - void openInspectionCertificate(); - void openMarketplace(); void enableEntityHighlight(const EntityItemID& entityItemID); void disableEntityHighlight(const EntityItemID& entityItemID); diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 6d2986768a..dab377911b 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -1,5 +1,5 @@ "use strict"; -/*global Tablet, Script*/ +/* global Tablet, Script */ // // libraries/appUi.js // @@ -11,6 +11,7 @@ // function AppUi(properties) { + var request = Script.require('request').request; /* Example development order: 1. var AppUi = Script.require('appUi'); 2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3). @@ -31,37 +32,63 @@ function AppUi(properties) { var that = this; function defaultButton(name, suffix) { var base = that[name] || (that.buttonPrefix + suffix); - that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); //poor man's merge + that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); // poor man's merge } // Defaults: that.tabletName = "com.highfidelity.interface.tablet.system"; that.inject = ""; that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below. + that.additionalAppScreens = []; that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen. - return (type === that.type) && that.currentUrl && (tabletUrl.indexOf(that.currentUrl) >= 0); // Actual url may have prefix or suffix. + // Actual url may have prefix or suffix. + return (type === that.currentVisibleScreenType) && + that.currentVisibleUrl && + ((that.home.indexOf(that.currentVisibleUrl) > -1) || + (that.additionalAppScreens.indexOf(that.currentVisibleUrl) > -1)); }; - that.setCurrentData = function setCurrentData(url) { - that.currentUrl = url; - that.type = /.qml$/.test(url) ? 'QML' : 'Web'; - } - that.open = function open(optionalUrl) { // How to open the app. + that.setCurrentVisibleScreenMetadata = function setCurrentVisibleScreenMetadata(type, url) { + that.currentVisibleScreenType = type; + that.currentVisibleUrl = url; + }; + that.open = function open(optionalUrl, optionalInject) { // How to open the app. var url = optionalUrl || that.home; - that.setCurrentData(url); - if (that.isQML()) { + var inject = optionalInject || that.inject; + + if (that.isQMLUrl(url)) { that.tablet.loadQMLSource(url); } else { - that.tablet.gotoWebScreen(url, that.inject); + that.tablet.gotoWebScreen(url, inject); + } + }; + // Opens some app on top of the current app (on desktop, opens new window) + that.openNewAppOnTop = function openNewAppOnTop(url, optionalInject) { + var inject = optionalInject || ""; + if (that.isQMLUrl(url)) { + that.tablet.loadQMLOnTop(url); + } else { + that.tablet.loadWebScreenOnTop(url, inject); } }; that.close = function close() { // How to close the app. - that.currentUrl = ""; + that.currentVisibleUrl = ""; // for toolbar-mode: go back to home screen, this will close the window. that.tablet.gotoHomeScreen(); }; that.buttonActive = function buttonActive(isActive) { // How to make the button active (white). that.button.editProperties({isActive: isActive}); }; + that.isQMLUrl = function isQMLUrl(url) { + var type = /.qml$/.test(url) ? 'QML' : 'Web'; + return type === 'QML'; + }; + that.isCurrentlyOnQMLScreen = function isCurrentlyOnQMLScreen() { + return that.currentVisibleScreenType === 'QML'; + }; + + // + // START Notification Handling Defaults + // that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button. // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true. that.button.editProperties({ @@ -69,16 +96,124 @@ function AppUi(properties) { activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton }); }; - that.isQML = function isQML() { // We set type property in onClick. - return that.type === 'QML'; + that.notificationPollTimeout = false; + that.notificationPollTimeoutMs = 60000; + that.notificationPollEndpoint = false; + that.notificationPollStopPaginatingConditionMet = false; + that.notificationDataProcessPage = function (data) { + return data; }; - that.eventSignal = function eventSignal() { // What signal to hook onMessage to. - return that.isQML() ? that.tablet.fromQml : that.tablet.webEventReceived; + that.notificationPollCallback = that.ignore; + that.notificationPollCaresAboutSince = false; + that.notificationInitialCallbackMade = false; + that.notificationDisplayBanner = function (message) { + Window.displayAnnouncement(message); + }; + // + // END Notification Handling Defaults + // + + // Handlers + that.onScreenChanged = function onScreenChanged(type, url) { + // Set isOpen, wireEventBridge, set buttonActive as appropriate, + // and finally call onOpened() or onClosed() IFF defined. + that.setCurrentVisibleScreenMetadata(type, url); + if (that.checkIsOpen(type, url)) { + that.wireEventBridge(true); + if (!that.isOpen) { + that.buttonActive(true); + if (that.onOpened) { + that.onOpened(); + } + that.isOpen = true; + } + } else { // Not us. Should we do something for type Home, Menu, and particularly Closed (meaning tablet hidden? + that.wireEventBridge(false); + if (that.isOpen) { + that.buttonActive(false); + if (that.onClosed) { + that.onClosed(); + } + that.isOpen = false; + } + } + console.debug(that.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + + "\nNew screen URL: " + url + "\nCurrent app open status: " + that.isOpen + "\n"); }; // Overwrite with the given properties: Object.keys(properties).forEach(function (key) { that[key] = properties[key]; }); + // + // START Notification Handling + // + var METAVERSE_BASE = Account.metaverseServerURL; + var currentDataPageToRetrieve = 1; + var concatenatedServerResponse = new Array(); + that.notificationPoll = function () { + if (!that.notificationPollEndpoint) { + return; + } + + // User is "appearing offline" + if (GlobalServices.findableBy === "none") { + that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); + return; + } + + var url = METAVERSE_BASE + that.notificationPollEndpoint; + + if (that.notificationPollCaresAboutSince) { + url = url + "&since=" + (new Date().getTime()); + } + + console.debug(that.buttonName, 'polling for notifications at endpoint', url); + + function requestCallback(error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to get", url, error || response.status); + that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); + return; + } + + if (!that.notificationPollStopPaginatingConditionMet || that.notificationPollStopPaginatingConditionMet(response)) { + that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); + + var notificationData; + if (concatenatedServerResponse.length) { + notificationData = concatenatedServerResponse; + } else { + notificationData = that.notificationDataProcessPage(response); + } + console.debug(that.buttonName, 'notification data for processing:', JSON.stringify(notificationData)); + that.notificationPollCallback(notificationData); + that.notificationInitialCallbackMade = true; + currentDataPageToRetrieve = 1; + concatenatedServerResponse = new Array(); + } else { + concatenatedServerResponse = concatenatedServerResponse.concat(that.notificationDataProcessPage(response)); + currentDataPageToRetrieve++; + request({ uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); + } + } + + request({ uri: url }, requestCallback); + }; + + // This won't do anything if there isn't a notification endpoint set + that.notificationPoll(); + + function availabilityChanged() { + if (that.notificationPollTimeout) { + Script.clearTimeout(that.notificationPollTimeout); + that.notificationPollTimeout = false; + } + that.notificationPoll(); + } + // + // END Notification Handling + // + // Properties: that.tablet = Tablet.getTablet(that.tabletName); // Must be after we gather properties. @@ -100,65 +235,58 @@ function AppUi(properties) { } that.button = that.tablet.addButton(buttonOptions); that.ignore = function ignore() { }; - - // Handlers - that.onScreenChanged = function onScreenChanged(type, url) { - // Set isOpen, wireEventBridge, set buttonActive as appropriate, - // and finally call onOpened() or onClosed() IFF defined. - console.debug(that.buttonName, 'onScreenChanged', type, url, that.isOpen); - if (that.checkIsOpen(type, url)) { - if (!that.isOpen) { - that.wireEventBridge(true); - that.buttonActive(true); - if (that.onOpened) { - that.onOpened(); - } - that.isOpen = true; - } - - } else { // Not us. Should we do something for type Home, Menu, and particularly Closed (meaning tablet hidden? - if (that.isOpen) { - that.wireEventBridge(false); - that.buttonActive(false); - if (that.onClosed) { - that.onClosed(); - } - that.isOpen = false; - } - } - }; - that.hasEventBridge = false; + that.hasOutboundEventBridge = false; + that.hasInboundQmlEventBridge = false; + that.hasInboundHtmlEventBridge = false; // HTML event bridge uses strings, not objects. Here we abstract over that. // (Although injected javascript still has to use JSON.stringify/JSON.parse.) - that.sendToHtml = function (messageObject) { that.tablet.emitScriptEvent(JSON.stringify(messageObject)); }; - that.fromHtml = function (messageString) { that.onMessage(JSON.parse(messageString)); }; + that.sendToHtml = function (messageObject) { + that.tablet.emitScriptEvent(JSON.stringify(messageObject)); + }; + that.fromHtml = function (messageString) { + var parsedMessage = JSON.parse(messageString); + parsedMessage.messageSrc = "HTML"; + that.onMessage(parsedMessage); + }; that.sendMessage = that.ignore; that.wireEventBridge = function wireEventBridge(on) { // Uniquivocally sets that.sendMessage(messageObject) to do the right thing. - // Sets hasEventBridge and wires onMessage to eventSignal as appropriate, IFF onMessage defined. - var handler, isQml = that.isQML(); + // Sets has*EventBridge and wires onMessage to the proper event bridge as appropriate, IFF onMessage defined. + var isCurrentlyOnQMLScreen = that.isCurrentlyOnQMLScreen(); // Outbound (always, regardless of whether there is an inbound handler). if (on) { - that.sendMessage = isQml ? that.tablet.sendToQml : that.sendToHtml; + that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml; + that.hasOutboundEventBridge = true; } else { that.sendMessage = that.ignore; + that.hasOutboundEventBridge = false; } - if (!that.onMessage) { return; } + if (!that.onMessage) { + return; + } // Inbound - handler = isQml ? that.onMessage : that.fromHtml; if (on) { - if (!that.hasEventBridge) { - console.debug(that.buttonName, 'connecting', that.eventSignal()); - that.eventSignal().connect(handler); - that.hasEventBridge = true; + if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.fromQml); + that.tablet.fromQml.connect(that.onMessage); + that.hasInboundQmlEventBridge = true; + } else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.connect(that.fromHtml); + that.hasInboundHtmlEventBridge = true; } } else { - if (that.hasEventBridge) { - console.debug(that.buttonName, 'disconnecting', that.eventSignal()); - that.eventSignal().disconnect(handler); - that.hasEventBridge = false; + if (that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml); + that.tablet.fromQml.disconnect(that.onMessage); + that.hasInboundQmlEventBridge = false; + } + if (that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.disconnect(that.fromHtml); + that.hasInboundHtmlEventBridge = false; } } }; @@ -175,6 +303,7 @@ function AppUi(properties) { } : that.ignore; that.onScriptEnding = function onScriptEnding() { // Close if necessary, clean up any remaining handlers, and remove the button. + GlobalServices.findableByChanged.disconnect(availabilityChanged); if (that.isOpen) { that.close(); } @@ -185,10 +314,15 @@ function AppUi(properties) { } that.tablet.removeButton(that.button); } + if (that.notificationPollTimeout) { + Script.clearInterval(that.notificationPollTimeout); + that.notificationPollTimeout = false; + } }; // Set up the handlers. that.tablet.screenChanged.connect(that.onScreenChanged); that.button.clicked.connect(that.onClicked); Script.scriptEnding.connect(that.onScriptEnding); + GlobalServices.findableByChanged.connect(availabilityChanged); } module.exports = AppUi; diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 5939b36438..993ea30c2e 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -1,5 +1,5 @@ "use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ +/* jslint vars:true, plusplus:true, forin:true */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // wallet.js @@ -14,629 +14,529 @@ /* global getConnectionData */ (function () { // BEGIN LOCAL_SCOPE - Script.include("/~/system/libraries/accountUtils.js"); - Script.include("/~/system/libraries/connectionUtils.js"); +Script.include("/~/system/libraries/accountUtils.js"); +Script.include("/~/system/libraries/connectionUtils.js"); +var AppUi = Script.require('appUi'); - var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; - // BEGIN AVATAR SELECTOR LOGIC - var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; - var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; - var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; +// BEGIN AVATAR SELECTOR LOGIC +var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; +var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; +var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; - var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. +var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. - function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. - overlays[key] = this; - this.key = key; - this.selected = false; - this.hovering = false; - this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... +function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. + overlays[key] = this; + this.key = key; + this.selected = false; + this.hovering = false; + this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... +} +// Instance methods: +ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay + Overlays.deleteOverlay(this.activeOverlay); + delete overlays[this.key]; +}; + +ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay + Overlays.editOverlay(this.activeOverlay, properties); +}; + +function color(selected, hovering) { + var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; + function scale(component) { + var delta = 0xFF - component; + return component; } - // Instance methods: - ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay - Overlays.deleteOverlay(this.activeOverlay); - delete overlays[this.key]; - }; - - ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay - Overlays.editOverlay(this.activeOverlay, properties); - }; - - function color(selected, hovering) { - var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; - function scale(component) { - var delta = 0xFF - component; - return component; - } - return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; - } - // so we don't have to traverse the overlays to get the last one - var lastHoveringId = 0; - ExtendedOverlay.prototype.hover = function (hovering) { - this.hovering = hovering; - if (this.key === lastHoveringId) { - if (hovering) { - return; - } - lastHoveringId = 0; - } - this.editOverlay({ color: color(this.selected, hovering) }); + return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; +} +// so we don't have to traverse the overlays to get the last one +var lastHoveringId = 0; +ExtendedOverlay.prototype.hover = function (hovering) { + this.hovering = hovering; + if (this.key === lastHoveringId) { if (hovering) { - // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId !== this.key) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - lastHoveringId = this.key; - } - }; - ExtendedOverlay.prototype.select = function (selected) { - if (this.selected === selected) { return; } - - this.editOverlay({ color: color(selected, this.hovering) }); - this.selected = selected; - }; - // Class methods: - var selectedId = false; - ExtendedOverlay.isSelected = function (id) { - return selectedId === id; - }; - ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier - return overlays[key]; - }; - ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. - var key; - for (key in overlays) { - if (iterator(ExtendedOverlay.get(key))) { - return; - } - } - }; - ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) - if (lastHoveringId) { + lastHoveringId = 0; + } + this.editOverlay({ color: color(this.selected, hovering) }); + if (hovering) { + // un-hover the last hovering overlay + if (lastHoveringId && lastHoveringId !== this.key) { ExtendedOverlay.get(lastHoveringId).hover(false); } - }; + lastHoveringId = this.key; + } +}; +ExtendedOverlay.prototype.select = function (selected) { + if (this.selected === selected) { + return; + } - // hit(overlay) on the one overlay intersected by pickRay, if any. - // noHit() if no ExtendedOverlay was intersected (helps with hover) - ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. - if (!pickedOverlay.intersects) { - if (noHit) { - return noHit(); - } + this.editOverlay({ color: color(selected, this.hovering) }); + this.selected = selected; +}; +// Class methods: +var selectedId = false; +ExtendedOverlay.isSelected = function (id) { + return selectedId === id; +}; +ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier + return overlays[key]; +}; +ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. + var key; + for (key in overlays) { + if (iterator(ExtendedOverlay.get(key))) { return; } - ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. - if ((overlay.activeOverlay) === pickedOverlay.overlayID) { - hit(overlay); - return true; - } - }); - }; - - function addAvatarNode(id) { - return new ExtendedOverlay(id, "sphere", { - drawInFront: true, - solid: true, - alpha: 0.8, - color: color(false, false), - ignoreRayIntersection: false - }); } - - var pingPong = true; - function updateOverlays() { - var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id) { - return; // don't update ourself, or avatars we're not interested in - } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); - if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - overlay = addAvatarNode(id); - } - var target = avatar.position; - var distance = Vec3.distance(target, eye); - var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } - - // move a bit in front, towards the camera - target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); - - // now bump it up a bit - target.y = target.y + offset; - - overlay.ping = pingPong; - overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering), - position: target, - dimensions: 0.032 * distance - }); - }); - pingPong = !pingPong; - ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) - if (overlay.ping === pingPong) { - overlay.deleteOverlay(); - } - }); - } - function removeOverlays() { - selectedId = false; - lastHoveringId = 0; - ExtendedOverlay.some(function (overlay) { - overlay.deleteOverlay(); - }); +}; +ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) + if (lastHoveringId) { + ExtendedOverlay.get(lastHoveringId).hover(false); } +}; - // - // Clicks. - // - function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - if (selectedId === id) { - var message = { - method: 'updateSelectedRecipientUsername', - userName: username === "" ? "unknown username" : username - }; - sendToQml(message); +// hit(overlay) on the one overlay intersected by pickRay, if any. +// noHit() if no ExtendedOverlay was intersected (helps with hover) +ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { + var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. + if (!pickedOverlay.intersects) { + if (noHit) { + return noHit(); } + return; } - function handleClick(pickRay) { - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - var nextSelectedStatus = !overlay.selected; - var avatarId = overlay.key; - selectedId = nextSelectedStatus ? avatarId : false; - if (nextSelectedStatus) { - Users.requestUsernameFromID(avatarId); - } - var message = { - method: 'selectRecipient', - id: avatarId, - isSelected: nextSelectedStatus, - displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', - userName: '' - }; - sendToQml(message); - - ExtendedOverlay.some(function (overlay) { - var id = overlay.key; - var selected = ExtendedOverlay.isSelected(id); - overlay.select(selected); - }); - + ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. + if ((overlay.activeOverlay) === pickedOverlay.overlayID) { + hit(overlay); return true; - }); - } - function handleMouseEvent(mousePressEvent) { // handleClick if we get one. - if (!mousePressEvent.isLeftButton) { - return; } - handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); + }); +}; + +function addAvatarNode(id) { + return new ExtendedOverlay(id, "sphere", { + drawInFront: true, + solid: true, + alpha: 0.8, + color: color(false, false), + ignoreRayIntersection: false + }); +} + +var pingPong = true; +function updateOverlays() { + var eye = Camera.position; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + if (!id) { + return; // don't update ourself, or avatars we're not interested in + } + var avatar = AvatarList.getAvatar(id); + if (!avatar) { + return; // will be deleted below if there had been an overlay. + } + var overlay = ExtendedOverlay.get(id); + if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. + overlay = addAvatarNode(id); + } + var target = avatar.position; + var distance = Vec3.distance(target, eye); + var offset = 0.2; + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can + if (headIndex > 0) { + offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; + } + + // move a bit in front, towards the camera + target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); + + // now bump it up a bit + target.y = target.y + offset; + + overlay.ping = pingPong; + overlay.editOverlay({ + color: color(ExtendedOverlay.isSelected(id), overlay.hovering), + position: target, + dimensions: 0.032 * distance + }); + }); + pingPong = !pingPong; + ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) + if (overlay.ping === pingPong) { + overlay.deleteOverlay(); + } + }); +} +function removeOverlays() { + selectedId = false; + lastHoveringId = 0; + ExtendedOverlay.some(function (overlay) { + overlay.deleteOverlay(); + }); +} + +// +// Clicks. +// +function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { + if (selectedId === id) { + var message = { + method: 'updateSelectedRecipientUsername', + userName: username === "" ? "unknown username" : username + }; + ui.sendMessage(message); } - function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - overlay.hover(true); - }, function () { +} +function handleClick(pickRay) { + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + var nextSelectedStatus = !overlay.selected; + var avatarId = overlay.key; + selectedId = nextSelectedStatus ? avatarId : false; + if (nextSelectedStatus) { + Users.requestUsernameFromID(avatarId); + } + var message = { + method: 'selectRecipient', + id: avatarId, + isSelected: nextSelectedStatus, + displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', + userName: '' + }; + ui.sendMessage(message); + + ExtendedOverlay.some(function (overlay) { + var id = overlay.key; + var selected = ExtendedOverlay.isSelected(id); + overlay.select(selected); + }); + + return true; + }); +} +function handleMouseEvent(mousePressEvent) { // handleClick if we get one. + if (!mousePressEvent.isLeftButton) { + return; + } + handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); +} +function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + overlay.hover(true); + }, function () { + ExtendedOverlay.unHover(); + }); +} + +// handy global to keep track of which hand is the mouse (if any) +var currentHandPressed = 0; +var TRIGGER_CLICK_THRESHOLD = 0.85; +var TRIGGER_PRESS_THRESHOLD = 0.05; + +function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position + var pickRay; + if (HMD.active) { + if (currentHandPressed !== 0) { + pickRay = controllerComputePickRay(currentHandPressed); + } else { + // nothing should hover, so ExtendedOverlay.unHover(); + return; + } + } else { + pickRay = Camera.computePickRay(event.x, event.y); + } + handleMouseMove(pickRay); +} +function handleTriggerPressed(hand, value) { + // The idea is if you press one trigger, it is the one + // we will consider the mouse. Even if the other is pressed, + // we ignore it until this one is no longer pressed. + var isPressed = value > TRIGGER_PRESS_THRESHOLD; + if (currentHandPressed === 0) { + currentHandPressed = isPressed ? hand : 0; + return; + } + if (currentHandPressed === hand) { + currentHandPressed = isPressed ? hand : 0; + return; + } + // otherwise, the other hand is still triggered + // so do nothing. +} + +// We get mouseMoveEvents from the handControllers, via handControllerPointer. +// But we don't get mousePressEvents. +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); +var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); +function controllerComputePickRay(hand) { + var controllerPose = getControllerWorldLocation(hand, true); + if (controllerPose.valid) { + return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; + } +} +function makeClickHandler(hand) { + return function (clicked) { + if (clicked > TRIGGER_CLICK_THRESHOLD) { + var pickRay = controllerComputePickRay(hand); + handleClick(pickRay); + } + }; +} +function makePressHandler(hand) { + return function (value) { + handleTriggerPressed(hand, value); + }; +} +triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); +triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); +triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +// END AVATAR SELECTOR LOGIC + +var sendMoneyRecipient; +var sendMoneyParticleEffectUpdateTimer; +var particleEffectTimestamp; +var sendMoneyParticleEffect; +var SEND_MONEY_PARTICLE_TIMER_UPDATE = 250; +var SEND_MONEY_PARTICLE_EMITTING_DURATION = 3000; +var SEND_MONEY_PARTICLE_LIFETIME_SECONDS = 8; +var SEND_MONEY_PARTICLE_PROPERTIES = { + accelerationSpread: { x: 0, y: 0, z: 0 }, + alpha: 1, + alphaFinish: 1, + alphaSpread: 0, + alphaStart: 1, + azimuthFinish: 0, + azimuthStart: -6, + color: { red: 143, green: 5, blue: 255 }, + colorFinish: { red: 255, green: 0, blue: 204 }, + colorSpread: { red: 0, green: 0, blue: 0 }, + colorStart: { red: 0, green: 136, blue: 255 }, + emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate + emitDimensions: { x: 0, y: 0, z: 0 }, + emitOrientation: { x: 0, y: 0, z: 0 }, + emitRate: 4, + emitSpeed: 2.1, + emitterShouldTrail: true, + isEmitting: 1, + lifespan: SEND_MONEY_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate + lifetime: SEND_MONEY_PARTICLE_LIFETIME_SECONDS + 1, + maxParticles: 20, + name: 'hfc-particles', + particleRadius: 0.2, + polarFinish: 0, + polarStart: 0, + radiusFinish: 0.05, + radiusSpread: 0, + radiusStart: 0.2, + speedSpread: 0, + textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", + type: 'ParticleEffect' +}; + +var MS_PER_SEC = 1000; +function updateSendMoneyParticleEffect() { + var timestampNow = Date.now(); + if ((timestampNow - particleEffectTimestamp) > (SEND_MONEY_PARTICLE_LIFETIME_SECONDS * MS_PER_SEC)) { + deleteSendMoneyParticleEffect(); + return; + } else if ((timestampNow - particleEffectTimestamp) > SEND_MONEY_PARTICLE_EMITTING_DURATION) { + Entities.editEntity(sendMoneyParticleEffect, { + isEmitting: 0 + }); + } else if (sendMoneyParticleEffect) { + var recipientPosition = AvatarList.getAvatar(sendMoneyRecipient).position; + var distance = Vec3.distance(recipientPosition, MyAvatar.position); + var accel = Vec3.subtract(recipientPosition, MyAvatar.position); + accel.y -= 3.0; + var life = Math.sqrt(2 * distance / Vec3.length(accel)); + Entities.editEntity(sendMoneyParticleEffect, { + emitAcceleration: accel, + lifespan: life }); } +} - // handy global to keep track of which hand is the mouse (if any) - var currentHandPressed = 0; - var TRIGGER_CLICK_THRESHOLD = 0.85; - var TRIGGER_PRESS_THRESHOLD = 0.05; - - function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position - var pickRay; - if (HMD.active) { - if (currentHandPressed !== 0) { - pickRay = controllerComputePickRay(currentHandPressed); - } else { - // nothing should hover, so - ExtendedOverlay.unHover(); - return; - } - } else { - pickRay = Camera.computePickRay(event.x, event.y); - } - handleMouseMove(pickRay); +function deleteSendMoneyParticleEffect() { + if (sendMoneyParticleEffectUpdateTimer) { + Script.clearInterval(sendMoneyParticleEffectUpdateTimer); + sendMoneyParticleEffectUpdateTimer = null; } - function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one - // we will consider the mouse. Even if the other is pressed, - // we ignore it until this one is no longer pressed. - var isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed === 0) { - currentHandPressed = isPressed ? hand : 0; - return; - } - if (currentHandPressed === hand) { - currentHandPressed = isPressed ? hand : 0; - return; - } - // otherwise, the other hand is still triggered - // so do nothing. + if (sendMoneyParticleEffect) { + sendMoneyParticleEffect = Entities.deleteEntity(sendMoneyParticleEffect); } + sendMoneyRecipient = null; +} - // We get mouseMoveEvents from the handControllers, via handControllerPointer. - // But we don't get mousePressEvents. - var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); - var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); - function controllerComputePickRay(hand) { - var controllerPose = getControllerWorldLocation(hand, true); - if (controllerPose.valid) { - return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; - } +function onUsernameChanged() { + if (Account.username !== Settings.getValue("wallet/savedUsername")) { + Settings.setValue("wallet/autoLogout", false); + Settings.setValue("wallet/savedUsername", ""); } - function makeClickHandler(hand) { - return function (clicked) { - if (clicked > TRIGGER_CLICK_THRESHOLD) { - var pickRay = controllerComputePickRay(hand); - handleClick(pickRay); - } - }; - } - function makePressHandler(hand) { - return function (value) { - handleTriggerPressed(hand, value); - }; - } - triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); - triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); - triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); - triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); - // END AVATAR SELECTOR LOGIC +} - // Function Name: onButtonClicked() - // - // Description: - // -Fired when the app button is pressed. - // - // Relevant Variables: - // -WALLET_QML_SOURCE: The path to the Wallet QML - // -onWalletScreen: true/false depending on whether we're looking at the app. - var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; - var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; - var onWalletScreen = false; - function onButtonClicked() { - if (!tablet) { - print("Warning in buttonClicked(): 'tablet' undefined!"); - return; +// Function Name: fromQml() +// +// Description: +// -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the QML +// in the format "{method, params}", like json-rpc. See also sendToQml(). +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +function fromQml(message) { + switch (message.method) { + case 'passphrasePopup_cancelClicked': + case 'needsLogIn_cancelClicked': + ui.close(); + break; + case 'walletSetup_cancelClicked': + switch (message.referrer) { + case '': // User clicked "Wallet" app + case undefined: + case null: + ui.close(); + break; + case 'purchases': + case 'marketplace cta': + case 'mainPage': + ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + default: // User needs to return to an individual marketplace item URL + ui.open(MARKETPLACE_URL + '/items/' + message.referrer, MARKETPLACES_INJECT_SCRIPT_URL); + break; } - if (onWalletScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - tablet.loadQMLSource(WALLET_QML_SOURCE); - } - } - - // Function Name: sendToQml() - // - // Description: - // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to - // the QML in the format "{method, params}", like json-rpc. See also fromQml(). - function sendToQml(message) { - tablet.sendToQml(message); - } - - var sendMoneyRecipient; - var sendMoneyParticleEffectUpdateTimer; - var particleEffectTimestamp; - var sendMoneyParticleEffect; - var SEND_MONEY_PARTICLE_TIMER_UPDATE = 250; - var SEND_MONEY_PARTICLE_EMITTING_DURATION = 3000; - var SEND_MONEY_PARTICLE_LIFETIME_SECONDS = 8; - var SEND_MONEY_PARTICLE_PROPERTIES = { - accelerationSpread: { x: 0, y: 0, z: 0 }, - alpha: 1, - alphaFinish: 1, - alphaSpread: 0, - alphaStart: 1, - azimuthFinish: 0, - azimuthStart: -6, - color: { red: 143, green: 5, blue: 255 }, - colorFinish: { red: 255, green: 0, blue: 204 }, - colorSpread: { red: 0, green: 0, blue: 0 }, - colorStart: { red: 0, green: 136, blue: 255 }, - emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate - emitDimensions: { x: 0, y: 0, z: 0 }, - emitOrientation: { x: 0, y: 0, z: 0 }, - emitRate: 4, - emitSpeed: 2.1, - emitterShouldTrail: true, - isEmitting: 1, - lifespan: SEND_MONEY_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate - lifetime: SEND_MONEY_PARTICLE_LIFETIME_SECONDS + 1, - maxParticles: 20, - name: 'hfc-particles', - particleRadius: 0.2, - polarFinish: 0, - polarStart: 0, - radiusFinish: 0.05, - radiusSpread: 0, - radiusStart: 0.2, - speedSpread: 0, - textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", - type: 'ParticleEffect' - }; - - function updateSendMoneyParticleEffect() { - var timestampNow = Date.now(); - if ((timestampNow - particleEffectTimestamp) > (SEND_MONEY_PARTICLE_LIFETIME_SECONDS * 1000)) { - deleteSendMoneyParticleEffect(); - return; - } else if ((timestampNow - particleEffectTimestamp) > SEND_MONEY_PARTICLE_EMITTING_DURATION) { - Entities.editEntity(sendMoneyParticleEffect, { - isEmitting: 0 - }); - } else if (sendMoneyParticleEffect) { - var recipientPosition = AvatarList.getAvatar(sendMoneyRecipient).position; - var distance = Vec3.distance(recipientPosition, MyAvatar.position); - var accel = Vec3.subtract(recipientPosition, MyAvatar.position); - accel.y -= 3.0; - var life = Math.sqrt(2 * distance / Vec3.length(accel)); - Entities.editEntity(sendMoneyParticleEffect, { - emitAcceleration: accel, - lifespan: life - }); - } - } - - function deleteSendMoneyParticleEffect() { - if (sendMoneyParticleEffectUpdateTimer) { - Script.clearInterval(sendMoneyParticleEffectUpdateTimer); - sendMoneyParticleEffectUpdateTimer = null; - } - if (sendMoneyParticleEffect) { - sendMoneyParticleEffect = Entities.deleteEntity(sendMoneyParticleEffect); - } - sendMoneyRecipient = null; - } - - function onUsernameChanged() { - if (Account.username !== Settings.getValue("wallet/savedUsername")) { - Settings.setValue("wallet/autoLogout", false); - Settings.setValue("wallet/savedUsername", ""); - } - } - - // Function Name: fromQml() - // - // Description: - // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the QML - // in the format "{method, params}", like json-rpc. See also sendToQml(). - var isHmdPreviewDisabled = true; - var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - - function fromQml(message) { - switch (message.method) { - case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': - tablet.gotoHomeScreen(); - break; - case 'walletSetup_cancelClicked': - switch (message.referrer) { - case '': // User clicked "Wallet" app - case undefined: - case null: - tablet.gotoHomeScreen(); - break; - case 'purchases': - case 'marketplace cta': - case 'mainPage': - tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - default: // User needs to return to an individual marketplace item URL - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.referrer, MARKETPLACES_INJECT_SCRIPT_URL); - break; - } - break; - case 'needsLogIn_loginClicked': - openLoginWindow(); - break; - case 'disableHmdPreview': - break; // do nothing here, handled in marketplaces.js - case 'maybeEnableHmdPreview': - break; // do nothing here, handled in marketplaces.js - case 'passphraseReset': - onButtonClicked(); - onButtonClicked(); - break; - case 'walletReset': - Settings.setValue("isFirstUseOfPurchases", true); - onButtonClicked(); - onButtonClicked(); - break; - case 'transactionHistory_linkClicked': - tablet.gotoWebScreen(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'goToPurchases_fromWalletHome': - case 'goToPurchases': - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - break; - case 'goToMarketplaceMainPage': - tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'goToMarketplaceItemPage': - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'refreshConnections': - print('Refreshing Connections...'); - getConnectionData(false); - break; - case 'enable_ChooseRecipientNearbyMode': - if (!isUpdateOverlaysWired) { - Script.update.connect(updateOverlays); - isUpdateOverlaysWired = true; - } - break; - case 'disable_ChooseRecipientNearbyMode': - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); - break; - case 'sendAsset_sendPublicly': - if (message.assetName === "") { - deleteSendMoneyParticleEffect(); - sendMoneyRecipient = message.recipient; - var amount = message.amount; - var props = SEND_MONEY_PARTICLE_PROPERTIES; - props.parentID = MyAvatar.sessionUUID; - props.position = MyAvatar.position; - props.position.y += 0.2; - if (message.effectImage) { - props.textures = message.effectImage; - } - sendMoneyParticleEffect = Entities.addEntity(props, true); - particleEffectTimestamp = Date.now(); - updateSendMoneyParticleEffect(); - sendMoneyParticleEffectUpdateTimer = Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); - } - break; - case 'transactionHistory_goToBank': - if (Account.metaverseServerURL.indexOf("staging") >= 0) { - Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. - } else { - Window.location = "hifi://BankOfHighFidelity"; - } - break; - case 'wallet_availableUpdatesReceived': - // NOP - break; - case 'http.request': - // Handled elsewhere, don't log. - break; - default: - print('Unrecognized message from QML:', JSON.stringify(message)); - } - } - - // Function Name: wireEventBridge() - // - // Description: - // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or - // disable to event bridge. - // - // Relevant Variables: - // -hasEventBridge: true/false depending on whether we've already connected the event bridge. - var hasEventBridge = false; - function wireEventBridge(on) { - if (!tablet) { - print("Warning in wireEventBridge(): 'tablet' undefined!"); - return; - } - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } - } - } - - // Function Name: onTabletScreenChanged() - // - // Description: - // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string - // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. - function onTabletScreenChanged(type, url) { - onWalletScreen = (type === "QML" && url === WALLET_QML_SOURCE); - wireEventBridge(onWalletScreen); - // Change button to active when window is first openend, false otherwise. - if (button) { - button.editProperties({ isActive: onWalletScreen }); - } - - if (onWalletScreen) { - if (!isWired) { - Users.usernameFromIDReply.connect(usernameFromIDReply); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - } - isWired = true; - } else { - off(); - } - } - - // - // Manage the connection between the button and the window. - // - var button; - var buttonName = "WALLET"; - var tablet = null; - var walletEnabled = Settings.getValue("commerce", true); - function startup() { - GlobalServices.myUsernameChanged.connect(onUsernameChanged); - if (walletEnabled) { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - text: buttonName, - icon: "icons/tablet-icons/wallet-i.svg", - activeIcon: "icons/tablet-icons/wallet-a.svg", - sortOrder: 10 - }); - button.clicked.connect(onButtonClicked); - tablet.screenChanged.connect(onTabletScreenChanged); - } - } - var isWired = false; - var isUpdateOverlaysWired = false; - function off() { - if (isWired) { - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - triggerMapping.disable(); - triggerPressMapping.disable(); - - isWired = false; + break; + case 'needsLogIn_loginClicked': + openLoginWindow(); + break; + case 'disableHmdPreview': + break; // do nothing here, handled in marketplaces.js + case 'maybeEnableHmdPreview': + break; // do nothing here, handled in marketplaces.js + case 'transactionHistory_linkClicked': + ui.open(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'goToPurchases_fromWalletHome': + case 'goToPurchases': + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + break; + case 'goToMarketplaceMainPage': + ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'goToMarketplaceItemPage': + ui.open(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'refreshConnections': + print('Refreshing Connections...'); + getConnectionData(false); + break; + case 'enable_ChooseRecipientNearbyMode': + if (!isUpdateOverlaysWired) { + Script.update.connect(updateOverlays); + isUpdateOverlaysWired = true; } + break; + case 'disable_ChooseRecipientNearbyMode': if (isUpdateOverlaysWired) { Script.update.disconnect(updateOverlays); isUpdateOverlaysWired = false; } removeOverlays(); - } - function shutdown() { - GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); - button.clicked.disconnect(onButtonClicked); - tablet.removeButton(button); - deleteSendMoneyParticleEffect(); - if (tablet) { - tablet.screenChanged.disconnect(onTabletScreenChanged); - if (onWalletScreen) { - tablet.gotoHomeScreen(); + break; + case 'sendAsset_sendPublicly': + if (message.assetName === "") { + deleteSendMoneyParticleEffect(); + sendMoneyRecipient = message.recipient; + var amount = message.amount; + var props = SEND_MONEY_PARTICLE_PROPERTIES; + props.parentID = MyAvatar.sessionUUID; + props.position = MyAvatar.position; + props.position.y += 0.2; + if (message.effectImage) { + props.textures = message.effectImage; } + sendMoneyParticleEffect = Entities.addEntity(props, true); + particleEffectTimestamp = Date.now(); + updateSendMoneyParticleEffect(); + sendMoneyParticleEffectUpdateTimer = Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); } - off(); + break; + case 'transactionHistory_goToBank': + if (Account.metaverseServerURL.indexOf("staging") >= 0) { + Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. + } else { + Window.location = "hifi://BankOfHighFidelity"; + } + break; + case 'wallet_availableUpdatesReceived': + // NOP + break; + case 'http.request': + // Handled elsewhere, don't log. + break; + default: + print('Unrecognized message from QML:', JSON.stringify(message)); } +} - // - // Run the functions. - // - startup(); - Script.scriptEnding.connect(shutdown); +function walletOpened() { + Users.usernameFromIDReply.connect(usernameFromIDReply); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); +} +function walletClosed() { + off(); +} + +// +// Manage the connection between the button and the window. +// +var BUTTON_NAME = "WALLET"; +var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; +var ui; +function startup() { + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 10, + home: WALLET_QML_SOURCE, + onOpened: walletOpened, + onClosed: walletClosed, + onMessage: fromQml + }); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); +} +var isUpdateOverlaysWired = false; +function off() { + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + triggerMapping.disable(); + triggerPressMapping.disable(); + + if (isUpdateOverlaysWired) { + Script.update.disconnect(updateOverlays); + isUpdateOverlaysWired = false; + } + removeOverlays(); +} +function shutdown() { + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); + deleteSendMoneyParticleEffect(); + off(); +} + +// +// Run the functions. +// +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/help.js b/scripts/system/help.js index aaeb82721c..40bbf6dbe2 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -1,5 +1,5 @@ "use strict"; - +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // help.js // scripts/system/ @@ -12,50 +12,18 @@ // /* globals Tablet, Script, HMD, Controller, Menu */ -(function() { // BEGIN LOCAL_SCOPE - - var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; - var HELP_URL = Script.resourcesPath() + "html/tabletHelp.html"; - var buttonName = "HELP"; - var onHelpScreen = false; - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var button = tablet.addButton({ - icon: "icons/tablet-icons/help-i.svg", - activeIcon: "icons/tablet-icons/help-a.svg", - text: buttonName, - sortOrder: 6 +(function () { // BEGIN LOCAL_SCOPE +var AppUi = Script.require('appUi'); + +var HELP_URL = Script.resourcesPath() + "html/tabletHelp.html"; +var HELP_BUTTON_NAME = "HELP"; +var ui; +function startup() { + ui = new AppUi({ + buttonName: HELP_BUTTON_NAME, + sortOrder: 6, + home: HELP_URL }); - - var enabled = false; - function onClicked() { - if (onHelpScreen) { - tablet.gotoHomeScreen(); - } else { - if (HMD.tabletID) { - Entities.editEntity(HMD.tabletID, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); - } - Menu.triggerOption('Help...'); - onHelpScreen = true; - } - } - - function onScreenChanged(type, url) { - onHelpScreen = type === "Web" && (url.indexOf(HELP_URL) === 0); - button.editProperties({ isActive: onHelpScreen }); - } - - button.clicked.connect(onClicked); - tablet.screenChanged.connect(onScreenChanged); - - Script.scriptEnding.connect(function () { - if (onHelpScreen) { - tablet.gotoHomeScreen(); - } - button.clicked.disconnect(onClicked); - tablet.screenChanged.disconnect(onScreenChanged); - if (tablet) { - tablet.removeButton(button); - } - }); - +} +startup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 7eece890c9..3239105254 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -1,3 +1,5 @@ +/* global $, window, MutationObserver */ + // // marketplacesInject.js // @@ -11,7 +13,6 @@ // (function () { - // Event bridge messages. var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; var CLARA_IO_STATUS = "CLARA.IO STATUS"; @@ -24,7 +25,7 @@ var canWriteAssets = false; var xmlHttpRequest = null; - var isPreparing = false; // Explicitly track download request status. + var isPreparing = false; // Explicitly track download request status. var commerceMode = false; var userIsLoggedIn = false; @@ -33,7 +34,6 @@ var messagesWaiting = false; function injectCommonCode(isDirectoryPage) { - // Supporting styles from marketplaces.css. // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. $("head").append( @@ -74,7 +74,9 @@ (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); }); $("#all-markets").on("click", function () { - EventBridge.emitWebEvent(GOTO_DIRECTORY); + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_DIRECTORY + })); }); } @@ -94,11 +96,11 @@ }); } - emitWalletSetupEvent = function() { + var emitWalletSetupEvent = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "WALLET_SETUP" })); - } + }; function maybeAddSetupWalletButton() { if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { @@ -285,7 +287,7 @@ $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); - var priceElement = $(this).find('.price') + var priceElement = $(this).find('.price'); priceElement.css({ "padding": "3px 5px", "height": "40px", @@ -355,12 +357,12 @@ function injectAddScrollbarToCategories() { $('#categories-dropdown').on('show.bs.dropdown', function () { $('body > div.container').css('display', 'none') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }) + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); }); $('#categories-dropdown').on('hide.bs.dropdown', function () { - $('body > div.container').css('display', '') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }) + $('body > div.container').css('display', ''); + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); }); } @@ -382,7 +384,6 @@ mutations.forEach(function (mutation) { injectBuyButtonOnMainPage(); }); - //observer.disconnect(); }); var config = { attributes: true, childList: true, characterData: true }; observer.observe(target, config); @@ -451,8 +452,8 @@ "itemPage", urlParams.get('edition'), type); - } - }); + } + }); maybeAddPurchasesButton(); } } @@ -503,127 +504,142 @@ $(".top-title .col-sm-4").append(downloadContainer); downloadContainer.append(downloadFBX); } + } + } - // Automatic download to High Fidelity. - function startAutoDownload() { + // Automatic download to High Fidelity. + function startAutoDownload() { + // One file request at a time. + if (isPreparing) { + console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); + return; + } - // One file request at a time. - if (isPreparing) { - console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); - return; - } + // User must be able to write to Asset Server. + if (!canWriteAssets) { + console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); + EventBridge.emitWebEvent(JSON.stringify({ + type: WARN_USER_NO_PERMISSIONS + })); + return; + } - // User must be able to write to Asset Server. - if (!canWriteAssets) { - console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); - EventBridge.emitWebEvent(WARN_USER_NO_PERMISSIONS); - return; - } + // User must be logged in. + var loginButton = $("#topnav a[href='/signup']"); + if (loginButton.length > 0) { + loginButton[0].click(); + return; + } - // User must be logged in. - var loginButton = $("#topnav a[href='/signup']"); - if (loginButton.length > 0) { - loginButton[0].click(); - return; - } + // Obtain zip file to download for requested asset. + // Reference: https://clara.io/learn/sdk/api/export - // Obtain zip file to download for requested asset. - // Reference: https://clara.io/learn/sdk/api/export + //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; + // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to + // be successful in generating zip files. + var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; - //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; - // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to - // be successful in generating zip files. - var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; + var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; + var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); - var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; - var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); + xmlHttpRequest = new XMLHttpRequest(); + var responseTextIndex = 0; + var zipFileURL = ""; - xmlHttpRequest = new XMLHttpRequest(); - var responseTextIndex = 0; - var zipFileURL = ""; + xmlHttpRequest.onreadystatechange = function () { + // Messages are appended to responseText; process the new ones. + var message = this.responseText.slice(responseTextIndex); + var statusMessage = ""; - xmlHttpRequest.onreadystatechange = function () { - // Messages are appended to responseText; process the new ones. - var message = this.responseText.slice(responseTextIndex); - var statusMessage = ""; + if (isPreparing) { // Ignore messages in flight after finished/cancelled. + var lines = message.split(/[\n\r]+/); - if (isPreparing) { // Ignore messages in flight after finished/cancelled. - var lines = message.split(/[\n\r]+/); + for (var i = 0, length = lines.length; i < length; i++) { + if (lines[i].slice(0, 5) === "data:") { + // Parse line. + var data; + try { + data = JSON.parse(lines[i].slice(5)); + } + catch (e) { + data = {}; + } - for (var i = 0, length = lines.length; i < length; i++) { - if (lines[i].slice(0, 5) === "data:") { - // Parse line. - var data; - try { - data = JSON.parse(lines[i].slice(5)); - } - catch (e) { - data = {}; - } + // Extract status message. + if (data.hasOwnProperty("message") && data.message !== null) { + statusMessage = data.message; + console.log("Clara.io FBX: " + statusMessage); + } - // Extract status message. - if (data.hasOwnProperty("message") && data.message !== null) { - statusMessage = data.message; - console.log("Clara.io FBX: " + statusMessage); - } - - // Extract zip file URL. - if (data.hasOwnProperty("files") && data.files.length > 0) { - zipFileURL = data.files[0].url; - if (zipFileURL.slice(-4) !== ".zip") { - console.log(JSON.stringify(data)); // Data for debugging. - } - } + // Extract zip file URL. + if (data.hasOwnProperty("files") && data.files.length > 0) { + zipFileURL = data.files[0].url; + if (zipFileURL.slice(-4) !== ".zip") { + console.log(JSON.stringify(data)); // Data for debugging. } } - - if (statusMessage !== "") { - // Update the UI with the most recent status message. - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } } - - responseTextIndex = this.responseText.length; - }; - - // Note: onprogress doesn't have computable total length so can't use it to determine % complete. - - xmlHttpRequest.onload = function () { - var statusMessage = ""; - - if (!isPreparing) { - return; - } - - isPreparing = false; - - var HTTP_OK = 200; - if (this.status !== HTTP_OK) { - statusMessage = "Zip file request terminated with " + this.status + " " + this.statusText; - console.log("ERROR: Clara.io FBX: " + statusMessage); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else if (zipFileURL.slice(-4) !== ".zip") { - statusMessage = "Error creating zip file for download."; - console.log("ERROR: Clara.io FBX: " + statusMessage + ": " + zipFileURL); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else { - EventBridge.emitWebEvent(CLARA_IO_DOWNLOAD + " " + zipFileURL); - console.log("Clara.io FBX: File download initiated for " + zipFileURL); - } - - xmlHttpRequest = null; } - isPreparing = true; - - console.log("Clara.io FBX: Request zip file for " + uuid); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " Initiating download"); - - xmlHttpRequest.open("POST", url, true); - xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); - xmlHttpRequest.send(); + if (statusMessage !== "") { + // Update the UI with the most recent status message. + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } } + + responseTextIndex = this.responseText.length; + }; + + // Note: onprogress doesn't have computable total length so can't use it to determine % complete. + + xmlHttpRequest.onload = function () { + var statusMessage = ""; + + if (!isPreparing) { + return; + } + + isPreparing = false; + + var HTTP_OK = 200; + if (this.status !== HTTP_OK) { + statusMessage = "Zip file request terminated with " + this.status + " " + this.statusText; + console.log("ERROR: Clara.io FBX: " + statusMessage); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } else if (zipFileURL.slice(-4) !== ".zip") { + statusMessage = "Error creating zip file for download."; + console.log("ERROR: Clara.io FBX: " + statusMessage + ": " + zipFileURL); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: (statusMessage + ": " + zipFileURL) + })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_DOWNLOAD + })); + console.log("Clara.io FBX: File download initiated for " + zipFileURL); + } + + xmlHttpRequest = null; } + + isPreparing = true; + + console.log("Clara.io FBX: Request zip file for " + uuid); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: "Initiating download" + })); + + xmlHttpRequest.open("POST", url, true); + xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); + xmlHttpRequest.send(); } function injectClaraCode() { @@ -663,7 +679,9 @@ updateClaraCodeInterval = undefined; }); - EventBridge.emitWebEvent(QUERY_CAN_WRITE_ASSETS); + EventBridge.emitWebEvent(JSON.stringify({ + type: QUERY_CAN_WRITE_ASSETS + })); } function cancelClaraDownload() { @@ -673,7 +691,9 @@ xmlHttpRequest.abort(); xmlHttpRequest = null; console.log("Clara.io FBX: File download cancelled"); - EventBridge.emitWebEvent(CLARA_IO_CANCELLED_DOWNLOAD); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_CANCELLED_DOWNLOAD + })); } } @@ -708,25 +728,22 @@ function onLoad() { EventBridge.scriptEventReceived.connect(function (message) { - if (message.slice(0, CAN_WRITE_ASSETS.length) === CAN_WRITE_ASSETS) { - canWriteAssets = message.slice(-4) === "true"; - } else if (message.slice(0, CLARA_IO_CANCEL_DOWNLOAD.length) === CLARA_IO_CANCEL_DOWNLOAD) { + message = JSON.parse(message); + if (message.type === CAN_WRITE_ASSETS) { + canWriteAssets = message.canWriteAssets; + } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { cancelClaraDownload(); - } else { - var parsedJsonMessage = JSON.parse(message); - - if (parsedJsonMessage.type === "marketplaces") { - if (parsedJsonMessage.action === "commerceSetting") { - commerceMode = !!parsedJsonMessage.data.commerceMode; - userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn; - walletNeedsSetup = !!parsedJsonMessage.data.walletNeedsSetup; - marketplaceBaseURL = parsedJsonMessage.data.metaverseServerURL; - if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { - marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); - } - messagesWaiting = parsedJsonMessage.data.messagesWaiting; - injectCode(); + } else if (message.type === "marketplaces") { + if (message.action === "commerceSetting") { + commerceMode = !!message.data.commerceMode; + userIsLoggedIn = !!message.data.userIsLoggedIn; + walletNeedsSetup = !!message.data.walletNeedsSetup; + marketplaceBaseURL = message.data.metaverseServerURL; + if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { + marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); } + messagesWaiting = message.data.messagesWaiting; + injectCode(); } } }); @@ -739,6 +756,6 @@ } // Load / unload. - window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed + window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed }()); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 7b4f05193f..13ad1f6b69 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -16,1147 +16,1082 @@ var selectionDisplay = null; // for gridTool.js to ignore (function () { // BEGIN LOCAL_SCOPE +var AppUi = Script.require('appUi'); +Script.include("/~/system/libraries/gridTool.js"); +Script.include("/~/system/libraries/connectionUtils.js"); - Script.include("/~/system/libraries/WebTablet.js"); - Script.include("/~/system/libraries/gridTool.js"); - Script.include("/~/system/libraries/connectionUtils.js"); +var METAVERSE_SERVER_URL = Account.metaverseServerURL; +var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; +var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); - var METAVERSE_SERVER_URL = Account.metaverseServerURL; - var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; - var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. - var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); - var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; - var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; - var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; - var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; - var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); +// Event bridge messages. +var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; +var CLARA_IO_STATUS = "CLARA.IO STATUS"; +var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; +var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; +var GOTO_DIRECTORY = "GOTO_DIRECTORY"; +var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; +var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; +var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; - var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; - // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +var CLARA_DOWNLOAD_TITLE = "Preparing Download"; +var messageBox = null; +var isDownloadBeingCancelled = false; - // Event bridge messages. - var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; - var CLARA_IO_STATUS = "CLARA.IO STATUS"; - var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; - var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; - var GOTO_DIRECTORY = "GOTO_DIRECTORY"; - var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; - var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; - var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; +var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel +var NO_BUTTON = 0; // QMessageBox::NoButton - var CLARA_DOWNLOAD_TITLE = "Preparing Download"; - var messageBox = null; - var isDownloadBeingCancelled = false; +var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; - var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel - var NO_BUTTON = 0; // QMessageBox::NoButton - - var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; - - function onMessageBoxClosed(id, button) { - if (id === messageBox && button === CANCEL_BUTTON) { - isDownloadBeingCancelled = true; - messageBox = null; - tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); - } - } - - var onMarketplaceScreen = false; - var onCommerceScreen = false; - - var debugCheckout = false; - var debugError = false; - function showMarketplace() { - if (!debugCheckout) { - UserActivityLogger.openedMarketplace(); - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - } else { - tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); - sendToQml({ - method: 'updateCheckoutQML', params: { - itemId: '424611a2-73d0-4c03-9087-26a6a279257b', - itemName: '2018-02-15 Finnegon', - itemPrice: (debugError ? 10 : 3), - itemHref: 'http://devmpassets.highfidelity.com/424611a2-73d0-4c03-9087-26a6a279257b-v1/finnigon.fst', - categories: ["Miscellaneous"] - } - }); - } - } - - function messagesWaiting(isWaiting) { - if (marketplaceButton) { - marketplaceButton.editProperties({ - icon: (isWaiting ? WAITING_ICON : NORMAL_ICON), - activeIcon: (isWaiting ? WAITING_ACTIVE : NORMAL_ACTIVE) - }); - } - } - - function onCanWriteAssetsChanged() { - var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - tablet.emitScriptEvent(message); - } - - - var tabletShouldBeVisibleInSecondaryCamera = false; - function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { - if (visibleInSecondaryCam) { - // if we're potentially showing the tablet, only do so if it was visible before - if (!tabletShouldBeVisibleInSecondaryCamera) { - return; - } - } else { - // if we're hiding the tablet, check to see if it was visible in the first place - tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); - } - - Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - } - - function openWallet() { - tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); - } - - function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { - wireEventBridge(true); - var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); - sendToQml({ - method: 'inspectionCertificate_setCertificateId', - entityId: currentEntityWithContextOverlay, - certificateId: certificateId +function onMessageBoxClosed(id, button) { + if (id === messageBox && button === CANCEL_BUTTON) { + isDownloadBeingCancelled = true; + messageBox = null; + ui.sendToHtml({ + type: CLARA_IO_CANCEL_DOWNLOAD }); } +} - function onUsernameChanged() { - if (onMarketplaceScreen) { - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - } - } +function onCanWriteAssetsChanged() { + ui.sendToHtml({ + type: CAN_WRITE_ASSETS, + canWriteAssets: Entities.canWriteAssets() + }); +} - var userHasUpdates = false; - function sendCommerceSettings() { - tablet.emitScriptEvent(JSON.stringify({ - type: "marketplaces", - action: "commerceSetting", - data: { - commerceMode: Settings.getValue("commerce", true), - userIsLoggedIn: Account.loggedIn, - walletNeedsSetup: Wallet.walletStatus === 1, - metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: userHasUpdates - } - })); - } - // BEGIN AVATAR SELECTOR LOGIC - var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; - var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; - var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; - - var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. - - function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. - overlays[key] = this; - this.key = key; - this.selected = false; - this.hovering = false; - this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... - } - // Instance methods: - ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay - Overlays.deleteOverlay(this.activeOverlay); - delete overlays[this.key]; - }; - - ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay - Overlays.editOverlay(this.activeOverlay, properties); - }; - - function color(selected, hovering) { - var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; - function scale(component) { - var delta = 0xFF - component; - return component; - } - return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; - } - // so we don't have to traverse the overlays to get the last one - var lastHoveringId = 0; - ExtendedOverlay.prototype.hover = function (hovering) { - this.hovering = hovering; - if (this.key === lastHoveringId) { - if (hovering) { - return; - } - lastHoveringId = 0; - } - this.editOverlay({ color: color(this.selected, hovering) }); - if (hovering) { - // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId !== this.key) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - lastHoveringId = this.key; - } - }; - ExtendedOverlay.prototype.select = function (selected) { - if (this.selected === selected) { +var tabletShouldBeVisibleInSecondaryCamera = false; +function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { + if (visibleInSecondaryCam) { + // if we're potentially showing the tablet, only do so if it was visible before + if (!tabletShouldBeVisibleInSecondaryCamera) { return; } + } else { + // if we're hiding the tablet, check to see if it was visible in the first place + tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); + } - this.editOverlay({ color: color(selected, this.hovering) }); - this.selected = selected; - }; - // Class methods: - var selectedId = false; - ExtendedOverlay.isSelected = function (id) { - return selectedId === id; - }; - ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier - return overlays[key]; - }; - ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. - var key; - for (key in overlays) { - if (iterator(ExtendedOverlay.get(key))) { - return; - } + Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); + Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); +} + +function openWallet() { + ui.open(MARKETPLACE_WALLET_QML_PATH); +} + +// Function Name: wireQmlEventBridge() +// +// Description: +// -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or +// disable to event bridge. +// +// Relevant Variables: +// -hasEventBridge: true/false depending on whether we've already connected the event bridge. +var hasEventBridge = false; +function wireQmlEventBridge(on) { + if (!ui.tablet) { + print("Warning in wireQmlEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + ui.tablet.fromQml.connect(onQmlMessageReceived); + hasEventBridge = true; } - }; - ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) - if (lastHoveringId) { + } else { + if (hasEventBridge) { + ui.tablet.fromQml.disconnect(onQmlMessageReceived); + hasEventBridge = false; + } + } +} + +var contextOverlayEntity = ""; +function openInspectionCertificateQML(currentEntityWithContextOverlay) { + ui.open(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH); + contextOverlayEntity = currentEntityWithContextOverlay; +} + +function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { + var certificateId = itemCertificateId || + (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); + ui.tablet.sendToQml({ + method: 'inspectionCertificate_setCertificateId', + entityId: currentEntityWithContextOverlay, + certificateId: certificateId + }); +} + +function onUsernameChanged() { + if (onMarketplaceScreen) { + ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + } +} + +var userHasUpdates = false; +function sendCommerceSettings() { + ui.sendToHtml({ + type: "marketplaces", + action: "commerceSetting", + data: { + commerceMode: Settings.getValue("commerce", true), + userIsLoggedIn: Account.loggedIn, + walletNeedsSetup: Wallet.walletStatus === 1, + metaverseServerURL: Account.metaverseServerURL, + messagesWaiting: userHasUpdates + } + }); +} + +// BEGIN AVATAR SELECTOR LOGIC +var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; +var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; +var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; + +var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. + +function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. + overlays[key] = this; + this.key = key; + this.selected = false; + this.hovering = false; + this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... +} +// Instance methods: +ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay + Overlays.deleteOverlay(this.activeOverlay); + delete overlays[this.key]; +}; + +ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay + Overlays.editOverlay(this.activeOverlay, properties); +}; + +function color(selected, hovering) { + var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; + function scale(component) { + return component; + } + return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; +} +// so we don't have to traverse the overlays to get the last one +var lastHoveringId = 0; +ExtendedOverlay.prototype.hover = function (hovering) { + this.hovering = hovering; + if (this.key === lastHoveringId) { + if (hovering) { + return; + } + lastHoveringId = 0; + } + this.editOverlay({ color: color(this.selected, hovering) }); + if (hovering) { + // un-hover the last hovering overlay + if (lastHoveringId && lastHoveringId !== this.key) { ExtendedOverlay.get(lastHoveringId).hover(false); } - }; + lastHoveringId = this.key; + } +}; +ExtendedOverlay.prototype.select = function (selected) { + if (this.selected === selected) { + return; + } - // hit(overlay) on the one overlay intersected by pickRay, if any. - // noHit() if no ExtendedOverlay was intersected (helps with hover) - ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. - if (!pickedOverlay.intersects) { - if (noHit) { - return noHit(); - } + this.editOverlay({ color: color(selected, this.hovering) }); + this.selected = selected; +}; +// Class methods: +var selectedId = false; +ExtendedOverlay.isSelected = function (id) { + return selectedId === id; +}; +ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier + return overlays[key]; +}; +ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. + var key; + for (key in overlays) { + if (iterator(ExtendedOverlay.get(key))) { return; } - ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. - if ((overlay.activeOverlay) === pickedOverlay.overlayID) { - hit(overlay); - return true; - } - }); - }; - - function addAvatarNode(id) { - return new ExtendedOverlay(id, "sphere", { - drawInFront: true, - solid: true, - alpha: 0.8, - color: color(false, false), - ignoreRayIntersection: false - }); } - - var pingPong = true; - function updateOverlays() { - var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id) { - return; // don't update ourself, or avatars we're not interested in - } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); - if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - overlay = addAvatarNode(id); - } - var target = avatar.position; - var distance = Vec3.distance(target, eye); - var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } - - // move a bit in front, towards the camera - target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); - - // now bump it up a bit - target.y = target.y + offset; - - overlay.ping = pingPong; - overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering), - position: target, - dimensions: 0.032 * distance - }); - }); - pingPong = !pingPong; - ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) - if (overlay.ping === pingPong) { - overlay.deleteOverlay(); - } - }); - } - function removeOverlays() { - selectedId = false; - lastHoveringId = 0; - ExtendedOverlay.some(function (overlay) { - overlay.deleteOverlay(); - }); +}; +ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) + if (lastHoveringId) { + ExtendedOverlay.get(lastHoveringId).hover(false); } +}; - // - // Clicks. - // - function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - if (selectedId === id) { - var message = { - method: 'updateSelectedRecipientUsername', - userName: username === "" ? "unknown username" : username - }; - sendToQml(message); +// hit(overlay) on the one overlay intersected by pickRay, if any. +// noHit() if no ExtendedOverlay was intersected (helps with hover) +ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { + var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. + if (!pickedOverlay.intersects) { + if (noHit) { + return noHit(); } + return; } - function handleClick(pickRay) { - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - var nextSelectedStatus = !overlay.selected; - var avatarId = overlay.key; - selectedId = nextSelectedStatus ? avatarId : false; - if (nextSelectedStatus) { - Users.requestUsernameFromID(avatarId); - } - var message = { - method: 'selectRecipient', - id: avatarId, - isSelected: nextSelectedStatus, - displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', - userName: '' - }; - sendToQml(message); - - ExtendedOverlay.some(function (overlay) { - var id = overlay.key; - var selected = ExtendedOverlay.isSelected(id); - overlay.select(selected); - }); - + ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. + if ((overlay.activeOverlay) === pickedOverlay.overlayID) { + hit(overlay); return true; - }); - } - function handleMouseEvent(mousePressEvent) { // handleClick if we get one. - if (!mousePressEvent.isLeftButton) { - return; } - handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); + }); +}; + +function addAvatarNode(id) { + return new ExtendedOverlay(id, "sphere", { + drawInFront: true, + solid: true, + alpha: 0.8, + color: color(false, false), + ignoreRayIntersection: false + }); +} + +var pingPong = true; +function updateOverlays() { + var eye = Camera.position; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + if (!id) { + return; // don't update ourself, or avatars we're not interested in + } + var avatar = AvatarList.getAvatar(id); + if (!avatar) { + return; // will be deleted below if there had been an overlay. + } + var overlay = ExtendedOverlay.get(id); + if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. + overlay = addAvatarNode(id); + } + var target = avatar.position; + var distance = Vec3.distance(target, eye); + var offset = 0.2; + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can + if (headIndex > 0) { + offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; + } + + // move a bit in front, towards the camera + target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); + + // now bump it up a bit + target.y = target.y + offset; + + overlay.ping = pingPong; + overlay.editOverlay({ + color: color(ExtendedOverlay.isSelected(id), overlay.hovering), + position: target, + dimensions: 0.032 * distance + }); + }); + pingPong = !pingPong; + ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) + if (overlay.ping === pingPong) { + overlay.deleteOverlay(); + } + }); +} +function removeOverlays() { + selectedId = false; + lastHoveringId = 0; + ExtendedOverlay.some(function (overlay) { + overlay.deleteOverlay(); + }); +} + +// +// Clicks. +// +function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { + if (selectedId === id) { + var message = { + method: 'updateSelectedRecipientUsername', + userName: username === "" ? "unknown username" : username + }; + ui.tablet.sendToQml(message); } - function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - overlay.hover(true); - }, function () { +} +function handleClick(pickRay) { + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + var nextSelectedStatus = !overlay.selected; + var avatarId = overlay.key; + selectedId = nextSelectedStatus ? avatarId : false; + if (nextSelectedStatus) { + Users.requestUsernameFromID(avatarId); + } + var message = { + method: 'selectRecipient', + id: avatarId, + isSelected: nextSelectedStatus, + displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', + userName: '' + }; + ui.tablet.sendToQml(message); + + ExtendedOverlay.some(function (overlay) { + var id = overlay.key; + var selected = ExtendedOverlay.isSelected(id); + overlay.select(selected); + }); + + return true; + }); +} +function handleMouseEvent(mousePressEvent) { // handleClick if we get one. + if (!mousePressEvent.isLeftButton) { + return; + } + handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); +} +function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + overlay.hover(true); + }, function () { + ExtendedOverlay.unHover(); + }); +} + +// handy global to keep track of which hand is the mouse (if any) +var currentHandPressed = 0; +var TRIGGER_CLICK_THRESHOLD = 0.85; +var TRIGGER_PRESS_THRESHOLD = 0.05; + +function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position + var pickRay; + if (HMD.active) { + if (currentHandPressed !== 0) { + pickRay = controllerComputePickRay(currentHandPressed); + } else { + // nothing should hover, so ExtendedOverlay.unHover(); - }); - } - - // handy global to keep track of which hand is the mouse (if any) - var currentHandPressed = 0; - var TRIGGER_CLICK_THRESHOLD = 0.85; - var TRIGGER_PRESS_THRESHOLD = 0.05; - - function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position - var pickRay; - if (HMD.active) { - if (currentHandPressed !== 0) { - pickRay = controllerComputePickRay(currentHandPressed); - } else { - // nothing should hover, so - ExtendedOverlay.unHover(); - return; - } - } else { - pickRay = Camera.computePickRay(event.x, event.y); - } - handleMouseMove(pickRay); - } - function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one - // we will consider the mouse. Even if the other is pressed, - // we ignore it until this one is no longer pressed. - var isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed === 0) { - currentHandPressed = isPressed ? hand : 0; return; } - if (currentHandPressed === hand) { - currentHandPressed = isPressed ? hand : 0; - return; - } - // otherwise, the other hand is still triggered - // so do nothing. + } else { + pickRay = Camera.computePickRay(event.x, event.y); } - - // We get mouseMoveEvents from the handControllers, via handControllerPointer. - // But we don't get mousePressEvents. - var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); - var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); - function controllerComputePickRay(hand) { - var controllerPose = getControllerWorldLocation(hand, true); - if (controllerPose.valid) { - return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; - } + handleMouseMove(pickRay); +} +function handleTriggerPressed(hand, value) { + // The idea is if you press one trigger, it is the one + // we will consider the mouse. Even if the other is pressed, + // we ignore it until this one is no longer pressed. + var isPressed = value > TRIGGER_PRESS_THRESHOLD; + if (currentHandPressed === 0) { + currentHandPressed = isPressed ? hand : 0; + return; } - function makeClickHandler(hand) { - return function (clicked) { - if (clicked > TRIGGER_CLICK_THRESHOLD) { - var pickRay = controllerComputePickRay(hand); - handleClick(pickRay); - } - }; + if (currentHandPressed === hand) { + currentHandPressed = isPressed ? hand : 0; + return; } - function makePressHandler(hand) { - return function (value) { - handleTriggerPressed(hand, value); - }; + // otherwise, the other hand is still triggered + // so do nothing. +} + +// We get mouseMoveEvents from the handControllers, via handControllerPointer. +// But we don't get mousePressEvents. +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); +var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); +function controllerComputePickRay(hand) { + var controllerPose = getControllerWorldLocation(hand, true); + if (controllerPose.valid) { + return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; } - triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); - triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); - triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); - triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); - // END AVATAR SELECTOR LOGIC - - var grid = new Grid(); - function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { - // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original - // position in the given direction. - var CORNERS = [ - { x: 0, y: 0, z: 0 }, - { x: 0, y: 0, z: 1 }, - { x: 0, y: 1, z: 0 }, - { x: 0, y: 1, z: 1 }, - { x: 1, y: 0, z: 0 }, - { x: 1, y: 0, z: 1 }, - { x: 1, y: 1, z: 0 }, - { x: 1, y: 1, z: 1 }, - ]; - - // Go through all corners and find least (most negative) distance in front of position. - var distance = 0; - for (var i = 0, length = CORNERS.length; i < length; i++) { - var cornerVector = - Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); - var cornerDistance = Vec3.dot(cornerVector, direction); - distance = Math.min(cornerDistance, distance); +} +function makeClickHandler(hand) { + return function (clicked) { + if (clicked > TRIGGER_CLICK_THRESHOLD) { + var pickRay = controllerComputePickRay(hand); + handleClick(pickRay); } - position = Vec3.sum(Vec3.multiply(distance, direction), position); - return position; - } - - var HALF_TREE_SCALE = 16384; - function getPositionToCreateEntity(extra) { - var CREATE_DISTANCE = 2; - var position; - var delta = extra !== undefined ? extra : 0; - if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); - } else { - position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); - position.y += 0.5; - } - - if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null; - } - return position; - } - - function rezEntity(itemHref, itemType) { - var isWearable = itemType === "wearable"; - var success = Clipboard.importEntities(itemHref); - var wearableLocalPosition = null; - var wearableLocalRotation = null; - var wearableLocalDimensions = null; - var wearableDimensions = null; - - if (itemType === "contentSet") { - console.log("Item is a content set; codepath shouldn't go here."); - return; - } - - if (isWearable) { - var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); - if (!wearableTransforms) { - // TODO delete this clause - wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms"); - } - var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? - if (certPos >= 0) { - certPos += 15; // length of "certificate_id=" - var certURLEncoded = itemHref.substring(certPos); - var certB64Encoded = decodeURIComponent(certURLEncoded); - for (var key in wearableTransforms) { - if (wearableTransforms.hasOwnProperty(key)) { - var certificateTransforms = wearableTransforms[key].certificateTransforms; - if (certificateTransforms) { - for (var certID in certificateTransforms) { - if (certificateTransforms.hasOwnProperty(certID) && - certID == certB64Encoded) { - var certificateTransform = certificateTransforms[certID]; - wearableLocalPosition = certificateTransform.localPosition; - wearableLocalRotation = certificateTransform.localRotation; - wearableLocalDimensions = certificateTransform.localDimensions; - wearableDimensions = certificateTransform.dimensions; - } - } - } - } - } - } - } - - if (success) { - var VERY_LARGE = 10000; - var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; - var position = Vec3.ZERO; - if (!isLargeImport) { - position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); - } - if (position !== null && position !== undefined) { - var pastedEntityIDs = Clipboard.pasteEntities(position); - if (!isLargeImport) { - // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move - // entities after they're imported so that they're all the correct distance in front of and with geometric mean - // centered on the avatar/camera direction. - var deltaPosition = Vec3.ZERO; - var entityPositions = []; - var entityParentIDs = []; - - var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; - var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; - if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { - var targetDirection; - if (Camera.mode === "entity" || Camera.mode === "independent") { - targetDirection = Camera.orientation; - } else { - targetDirection = MyAvatar.orientation; - } - targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); - - var targetPosition = getPositionToCreateEntity(); - var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. - var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. - for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", - "registrationPoint", "rotation", "parentID"]); - var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, - curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); - var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); - var distance = Vec3.dot(delta, targetDirection); - deltaParallel = Math.min(distance, deltaParallel); - deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), - deltaPerpendicular); - entityPositions[i] = curLoopEntityProps.position; - entityParentIDs[i] = curLoopEntityProps.parentID; - } - deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); - deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); - } - - if (grid.getSnapToGrid()) { - var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", - "registrationPoint"]); - var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); - position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, - firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); - deltaPosition = Vec3.subtract(position, firstEntityProps.position); - } - - if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { - if (Uuid.isNull(entityParentIDs[editEntityIndex])) { - Entities.editEntity(pastedEntityIDs[editEntityIndex], { - position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) - }); - } - } - } - } - - if (isWearable) { - // apply the relative offsets saved during checkout - var offsets = {}; - if (wearableLocalPosition) { - offsets.localPosition = wearableLocalPosition; - } - if (wearableLocalRotation) { - offsets.localRotation = wearableLocalRotation; - } - if (wearableLocalDimensions) { - offsets.localDimensions = wearableLocalDimensions; - } else if (wearableDimensions) { - offsets.dimensions = wearableDimensions; - } - // we currently assume a wearable is a single entity - Entities.editEntity(pastedEntityIDs[0], offsets); - } - - var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; - - Audio.playSound(REZZING_SOUND, { - volume: 1.0, - position: rezPosition, - localOnly: true - }); - - } else { - Window.notifyEditError("Can't import entities: entities would be out of bounds."); - } - } else { - Window.notifyEditError("There was an error importing the entity file."); - } - } - - var referrerURL; // Used for updating Purchases QML - var filterText; // Used for updating Purchases QML - function onMessage(message) { - if (message === GOTO_DIRECTORY) { - tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); - } else if (message === QUERY_CAN_WRITE_ASSETS) { - tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); - } else if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - } else if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { - return; - } - - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } - return; - } else if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; - } - return; - } else if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; - } else { - var parsedJsonMessage = JSON.parse(message); - if (parsedJsonMessage.type === "CHECKOUT") { - wireEventBridge(true); - tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); - sendToQml({ - method: 'updateCheckoutQML', - params: parsedJsonMessage - }); - } else if (parsedJsonMessage.type === "REQUEST_SETTING") { - sendCommerceSettings(); - } else if (parsedJsonMessage.type === "PURCHASES") { - referrerURL = parsedJsonMessage.referrerURL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - } else if (parsedJsonMessage.type === "LOGIN") { - openLoginWindow(); - } else if (parsedJsonMessage.type === "WALLET_SETUP") { - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: "marketplace cta" - }); - openWallet(); - } else if (parsedJsonMessage.type === "MY_ITEMS") { - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - wireEventBridge(true); - sendToQml({ - method: 'purchases_showMyItems' - }); - } - } - } - - function onButtonClicked() { - if (!tablet) { - print("Warning in buttonClicked(): 'tablet' undefined!"); - return; - } - if (onMarketplaceScreen || onCommerceScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - if (HMD.tabletID) { - Entities.editEntity(HMD.tabletID, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); - } - showMarketplace(); - } - } - - // Function Name: sendToQml() - // - // Description: - // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to - // the QML in the format "{method, params}", like json-rpc. See also fromQml(). - function sendToQml(message) { - tablet.sendToQml(message); - } - - var sendAssetRecipient; - var sendAssetParticleEffectUpdateTimer; - var particleEffectTimestamp; - var sendAssetParticleEffect; - var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250; - var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000; - var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8; - var SEND_ASSET_PARTICLE_PROPERTIES = { - accelerationSpread: { x: 0, y: 0, z: 0 }, - alpha: 1, - alphaFinish: 1, - alphaSpread: 0, - alphaStart: 1, - azimuthFinish: 0, - azimuthStart: -6, - color: { red: 255, green: 222, blue: 255 }, - colorFinish: { red: 255, green: 229, blue: 225 }, - colorSpread: { red: 0, green: 0, blue: 0 }, - colorStart: { red: 243, green: 255, blue: 255 }, - emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate - emitDimensions: { x: 0, y: 0, z: 0 }, - emitOrientation: { x: 0, y: 0, z: 0 }, - emitRate: 4, - emitSpeed: 2.1, - emitterShouldTrail: true, - isEmitting: 1, - lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate - lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, - maxParticles: 20, - name: 'asset-particles', - particleRadius: 0.2, - polarFinish: 0, - polarStart: 0, - radiusFinish: 0.05, - radiusSpread: 0, - radiusStart: 0.2, - speedSpread: 0, - textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", - type: 'ParticleEffect' }; +} +function makePressHandler(hand) { + return function (value) { + handleTriggerPressed(hand, value); + }; +} +triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); +triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); +triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +// END AVATAR SELECTOR LOGIC - function updateSendAssetParticleEffect() { - var timestampNow = Date.now(); - if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) { - deleteSendAssetParticleEffect(); - return; - } else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) { - Entities.editEntity(sendAssetParticleEffect, { - isEmitting: 0 - }); - } else if (sendAssetParticleEffect) { - var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position; - var distance = Vec3.distance(recipientPosition, MyAvatar.position); - var accel = Vec3.subtract(recipientPosition, MyAvatar.position); - accel.y -= 3.0; - var life = Math.sqrt(2 * distance / Vec3.length(accel)); - Entities.editEntity(sendAssetParticleEffect, { - emitAcceleration: accel, - lifespan: life +var grid = new Grid(); +function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { + // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // position in the given direction. + var CORNERS = [ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0, y: 1, z: 0 }, + { x: 0, y: 1, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 1, z: 0 }, + { x: 1, y: 1, z: 1 } + ]; + + // Go through all corners and find least (most negative) distance in front of position. + var distance = 0; + for (var i = 0, length = CORNERS.length; i < length; i++) { + var cornerVector = + Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); + var cornerDistance = Vec3.dot(cornerVector, direction); + distance = Math.min(cornerDistance, distance); + } + position = Vec3.sum(Vec3.multiply(distance, direction), position); + return position; +} + +var HALF_TREE_SCALE = 16384; +function getPositionToCreateEntity(extra) { + var CREATE_DISTANCE = 2; + var position; + var delta = extra !== undefined ? extra : 0; + if (Camera.mode === "entity" || Camera.mode === "independent") { + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); + } else { + position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); + position.y += 0.5; + } + + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { + return null; + } + return position; +} + +function rezEntity(itemHref, itemType) { + var isWearable = itemType === "wearable"; + var success = Clipboard.importEntities(itemHref); + var wearableLocalPosition = null; + var wearableLocalRotation = null; + var wearableLocalDimensions = null; + var wearableDimensions = null; + + if (itemType === "contentSet") { + console.log("Item is a content set; codepath shouldn't go here."); + return; + } + + if (isWearable) { + var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); + if (!wearableTransforms) { + // TODO delete this clause + wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms"); + } + var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? + if (certPos >= 0) { + certPos += 15; // length of "certificate_id=" + var certURLEncoded = itemHref.substring(certPos); + var certB64Encoded = decodeURIComponent(certURLEncoded); + for (var key in wearableTransforms) { + if (wearableTransforms.hasOwnProperty(key)) { + var certificateTransforms = wearableTransforms[key].certificateTransforms; + if (certificateTransforms) { + for (var certID in certificateTransforms) { + if (certificateTransforms.hasOwnProperty(certID) && + certID == certB64Encoded) { + var certificateTransform = certificateTransforms[certID]; + wearableLocalPosition = certificateTransform.localPosition; + wearableLocalRotation = certificateTransform.localRotation; + wearableLocalDimensions = certificateTransform.localDimensions; + wearableDimensions = certificateTransform.dimensions; + } + } + } + } + } + } + } + + if (success) { + var VERY_LARGE = 10000; + var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; + var position = Vec3.ZERO; + if (!isLargeImport) { + position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); + } + if (position !== null && position !== undefined) { + var pastedEntityIDs = Clipboard.pasteEntities(position); + if (!isLargeImport) { + // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move + // entities after they're imported so that they're all the correct distance in front of and with geometric mean + // centered on the avatar/camera direction. + var deltaPosition = Vec3.ZERO; + var entityPositions = []; + var entityParentIDs = []; + + var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; + var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; + if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { + var targetDirection; + if (Camera.mode === "entity" || Camera.mode === "independent") { + targetDirection = Camera.orientation; + } else { + targetDirection = MyAvatar.orientation; + } + targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); + + var targetPosition = getPositionToCreateEntity(); + var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. + var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. + for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { + var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + "registrationPoint", "rotation", "parentID"]); + var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, + curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); + var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); + var distance = Vec3.dot(delta, targetDirection); + deltaParallel = Math.min(distance, deltaParallel); + deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), + deltaPerpendicular); + entityPositions[i] = curLoopEntityProps.position; + entityParentIDs[i] = curLoopEntityProps.parentID; + } + deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); + deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); + } + + if (grid.getSnapToGrid()) { + var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + "registrationPoint"]); + var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); + position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, + firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); + deltaPosition = Vec3.subtract(position, firstEntityProps.position); + } + + if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { + for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + if (Uuid.isNull(entityParentIDs[editEntityIndex])) { + Entities.editEntity(pastedEntityIDs[editEntityIndex], { + position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) + }); + } + } + } + } + + if (isWearable) { + // apply the relative offsets saved during checkout + var offsets = {}; + if (wearableLocalPosition) { + offsets.localPosition = wearableLocalPosition; + } + if (wearableLocalRotation) { + offsets.localRotation = wearableLocalRotation; + } + if (wearableLocalDimensions) { + offsets.localDimensions = wearableLocalDimensions; + } else if (wearableDimensions) { + offsets.dimensions = wearableDimensions; + } + // we currently assume a wearable is a single entity + Entities.editEntity(pastedEntityIDs[0], offsets); + } + + var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; + + Audio.playSound(REZZING_SOUND, { + volume: 1.0, + position: rezPosition, + localOnly: true }); + + } else { + Window.notifyEditError("Can't import entities: entities would be out of bounds."); } + } else { + Window.notifyEditError("There was an error importing the entity file."); } +} - function deleteSendAssetParticleEffect() { - if (sendAssetParticleEffectUpdateTimer) { - Script.clearInterval(sendAssetParticleEffectUpdateTimer); - sendAssetParticleEffectUpdateTimer = null; - } - if (sendAssetParticleEffect) { - sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect); - } - sendAssetRecipient = null; - } - - var savedDisablePreviewOptionLocked = false; - var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview");; - function maybeEnableHMDPreview() { - // Set a small timeout to prevent sensitive data from being shown during - // UI fade - Script.setTimeout(function () { - setTabletVisibleInSecondaryCamera(true); - DesktopPreviewProvider.setPreviewDisabledReason("USER"); - Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption); - savedDisablePreviewOptionLocked = false; - }, 150); - } - - // Function Name: fromQml() - // - // Description: - // -Called when a message is received from Checkout.qml. The "message" argument is what is sent from the Checkout QML - // in the format "{method, params}", like json-rpc. - function fromQml(message) { - switch (message.method) { - case 'purchases_openWallet': - case 'checkout_openWallet': - case 'checkout_setUpClicked': - openWallet(); - break; - case 'purchases_walletNotSetUp': - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: "purchases" - }); - openWallet(); - break; - case 'checkout_walletNotSetUp': - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: message.referrer === "itemPage" ? message.itemId : message.referrer - }); - openWallet(); - break; - case 'checkout_cancelClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.params, MARKETPLACES_INJECT_SCRIPT_URL); - // TODO: Make Marketplace a QML app that's a WebView wrapper so we can use the app stack. - // I don't think this is trivial to do since we also want to inject some JS into the DOM. - //tablet.popFromStack(); - break; - case 'header_goToPurchases': - case 'checkout_goToPurchases': - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = message.filterText; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - break; - case 'checkout_itemLinkClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'checkout_continueShopping': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - //tablet.popFromStack(); - break; - case 'purchases_itemInfoClicked': - var itemId = message.itemId; - if (itemId && itemId !== "") { - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + itemId, MARKETPLACES_INJECT_SCRIPT_URL); - } - break; - case 'checkout_rezClicked': - case 'purchases_rezClicked': - rezEntity(message.itemHref, message.itemType); - break; - case 'header_marketplaceImageClicked': - case 'purchases_backClicked': - tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'purchases_goToMarketplaceClicked': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'updateItemClicked': - tablet.gotoWebScreen(message.upgradeUrl + "?edition=" + message.itemEdition, - MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'giftAsset': - - break; - case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'needsLogIn_loginClicked': - openLoginWindow(); - break; - case 'disableHmdPreview': - if (!savedDisablePreviewOption) { - savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); - savedDisablePreviewOptionLocked = true; - } - - if (!savedDisablePreviewOption) { - DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); - Menu.setIsOptionChecked("Disable Preview", true); - setTabletVisibleInSecondaryCamera(false); - } - break; - case 'maybeEnableHmdPreview': - maybeEnableHMDPreview(); - break; - case 'purchases_openGoTo': - tablet.loadQMLSource("hifi/tablet/TabletAddressDialog.qml"); - break; - case 'purchases_itemCertificateClicked': - setCertificateInfo("", message.itemCertificateId); - break; - case 'inspectionCertificate_closeClicked': - tablet.gotoHomeScreen(); - break; - case 'inspectionCertificate_requestOwnershipVerification': - ContextOverlay.requestOwnershipVerification(message.entity); - break; - case 'inspectionCertificate_showInMarketplaceClicked': - tablet.gotoWebScreen(message.marketplaceUrl, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'header_myItemsClicked': - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - wireEventBridge(true); - sendToQml({ - method: 'purchases_showMyItems' - }); - break; - case 'refreshConnections': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - print('Refreshing Connections...'); - getConnectionData(false); - } - break; - case 'enable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (!isUpdateOverlaysWired) { - Script.update.connect(updateOverlays); - isUpdateOverlaysWired = true; - } - } - break; - case 'disable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); - } - break; - case 'wallet_availableUpdatesReceived': - case 'purchases_availableUpdatesReceived': - userHasUpdates = message.numUpdates > 0; - messagesWaiting(userHasUpdates); - break; - case 'purchases_updateWearables': - var currentlyWornWearables = []; - var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) - - var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); - - for (var i = 0; i < nearbyEntities.length; i++) { - var currentProperties = Entities.getEntityProperties(nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID']); - if (currentProperties.parentID === MyAvatar.sessionUUID) { - currentlyWornWearables.push({ - entityID: nearbyEntities[i], - entityCertID: currentProperties.certificateID, - entityEdition: currentProperties.editionNumber - }); - } - } - - sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); - break; - case 'sendAsset_sendPublicly': - if (message.assetName !== "") { - deleteSendAssetParticleEffect(); - sendAssetRecipient = message.recipient; - var amount = message.amount; - var props = SEND_ASSET_PARTICLE_PROPERTIES; - props.parentID = MyAvatar.sessionUUID; - props.position = MyAvatar.position; - props.position.y += 0.2; - if (message.effectImage) { - props.textures = message.effectImage; - } - sendAssetParticleEffect = Entities.addEntity(props, true); - particleEffectTimestamp = Date.now(); - updateSendAssetParticleEffect(); - sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, SEND_ASSET_PARTICLE_TIMER_UPDATE); - } - break; - case 'http.request': - // Handled elsewhere, don't log. - break; - case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? - break; - default: - print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); - } - } - - // Function Name: wireEventBridge() - // - // Description: - // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or - // disable to event bridge. - // - // Relevant Variables: - // -hasEventBridge: true/false depending on whether we've already connected the event bridge. - var hasEventBridge = false; - function wireEventBridge(on) { - if (!tablet) { - print("Warning in wireEventBridge(): 'tablet' undefined!"); +var referrerURL; // Used for updating Purchases QML +var filterText; // Used for updating Purchases QML +function onWebEventReceived(message) { + message = JSON.parse(message); + if (message.type === GOTO_DIRECTORY) { + ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } else if (message.type === QUERY_CAN_WRITE_ASSETS) { + ui.sendToHtml(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } else if (message.type === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } else if (message.type === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { return; } - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } + + var text = message.status; + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } - } - - // Function Name: onTabletScreenChanged() - // - // Description: - // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string - // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. - var onWalletScreen = false; - var onCommerceScreen = false; - function onTabletScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; - var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; - var onCommerceScreenNow = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH - || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); - - if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) { // exiting wallet or commerce screen - maybeEnableHMDPreview(); + return; + } else if (message.type === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; } - - onCommerceScreen = onCommerceScreenNow; - onWalletScreen = onWalletScreenNow; - wireEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); - - if (url === MARKETPLACE_PURCHASES_QML_PATH) { - sendToQml({ - method: 'updatePurchases', - referrerURL: referrerURL, - filterText: filterText - }); - } - - // for toolbar mode: change button to active when window is first openend, false otherwise. - if (marketplaceButton) { - marketplaceButton.editProperties({ isActive: (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen }); - } - if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { - ContextOverlay.isInMarketplaceInspectionMode = true; - } else { - ContextOverlay.isInMarketplaceInspectionMode = false; - } - - if (onCommerceScreen) { - if (!isWired) { - Users.usernameFromIDReply.connect(usernameFromIDReply); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - } - isWired = true; - Wallet.refreshWalletStatus(); - } else { - off(); - sendToQml({ - method: 'inspectionCertificate_resetCert' - }); - } - } - - // - // Manage the connection between the button and the window. - // - var marketplaceButton; - var buttonName = "MARKET"; - var tablet = null; - var NORMAL_ICON = "icons/tablet-icons/market-i.svg"; - var NORMAL_ACTIVE = "icons/tablet-icons/market-a.svg"; - var WAITING_ICON = "icons/tablet-icons/market-i-msg.svg"; - var WAITING_ACTIVE = "icons/tablet-icons/market-a-msg.svg"; - function startup() { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - marketplaceButton = tablet.addButton({ - icon: NORMAL_ICON, - activeIcon: NORMAL_ACTIVE, - text: buttonName, - sortOrder: 9 + return; + } else if (message.type === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; + } else if (message.type === "CHECKOUT") { + wireQmlEventBridge(true); + ui.open(MARKETPLACE_CHECKOUT_QML_PATH); + ui.tablet.sendToQml({ + method: 'updateCheckoutQML', + params: message + }); + } else if (message.type === "REQUEST_SETTING") { + sendCommerceSettings(); + } else if (message.type === "PURCHASES") { + referrerURL = message.referrerURL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + } else if (message.type === "LOGIN") { + openLoginWindow(); + } else if (message.type === "WALLET_SETUP") { + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: "marketplace cta" + }); + openWallet(); + } else if (message.type === "MY_ITEMS") { + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'purchases_showMyItems' }); - - ContextOverlay.contextOverlayClicked.connect(setCertificateInfo); - Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); - GlobalServices.myUsernameChanged.connect(onUsernameChanged); - marketplaceButton.clicked.connect(onButtonClicked); - tablet.screenChanged.connect(onTabletScreenChanged); - tablet.webEventReceived.connect(onMessage); - Wallet.walletStatusChanged.connect(sendCommerceSettings); - Window.messageBoxClosed.connect(onMessageBoxClosed); - - Wallet.refreshWalletStatus(); } - var isWired = false; - var isUpdateOverlaysWired = false; - function off() { - if (isWired) { - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - triggerMapping.disable(); - triggerPressMapping.disable(); +} +var sendAssetRecipient; +var sendAssetParticleEffectUpdateTimer; +var particleEffectTimestamp; +var sendAssetParticleEffect; +var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250; +var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000; +var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8; +var SEND_ASSET_PARTICLE_PROPERTIES = { + accelerationSpread: { x: 0, y: 0, z: 0 }, + alpha: 1, + alphaFinish: 1, + alphaSpread: 0, + alphaStart: 1, + azimuthFinish: 0, + azimuthStart: -6, + color: { red: 255, green: 222, blue: 255 }, + colorFinish: { red: 255, green: 229, blue: 225 }, + colorSpread: { red: 0, green: 0, blue: 0 }, + colorStart: { red: 243, green: 255, blue: 255 }, + emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate + emitDimensions: { x: 0, y: 0, z: 0 }, + emitOrientation: { x: 0, y: 0, z: 0 }, + emitRate: 4, + emitSpeed: 2.1, + emitterShouldTrail: true, + isEmitting: 1, + lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate + lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, + maxParticles: 20, + name: 'asset-particles', + particleRadius: 0.2, + polarFinish: 0, + polarStart: 0, + radiusFinish: 0.05, + radiusSpread: 0, + radiusStart: 0.2, + speedSpread: 0, + textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", + type: 'ParticleEffect' +}; - isWired = false; - } - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); - } - function shutdown() { - maybeEnableHMDPreview(); +function updateSendAssetParticleEffect() { + var timestampNow = Date.now(); + if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) { deleteSendAssetParticleEffect(); + return; + } else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) { + Entities.editEntity(sendAssetParticleEffect, { + isEmitting: 0 + }); + } else if (sendAssetParticleEffect) { + var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position; + var distance = Vec3.distance(recipientPosition, MyAvatar.position); + var accel = Vec3.subtract(recipientPosition, MyAvatar.position); + accel.y -= 3.0; + var life = Math.sqrt(2 * distance / Vec3.length(accel)); + Entities.editEntity(sendAssetParticleEffect, { + emitAcceleration: accel, + lifespan: life + }); + } +} - ContextOverlay.contextOverlayClicked.disconnect(setCertificateInfo); - Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); - GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); - marketplaceButton.clicked.disconnect(onButtonClicked); - tablet.removeButton(marketplaceButton); - tablet.webEventReceived.disconnect(onMessage); - Wallet.walletStatusChanged.disconnect(sendCommerceSettings); - Window.messageBoxClosed.disconnect(onMessageBoxClosed); +function deleteSendAssetParticleEffect() { + if (sendAssetParticleEffectUpdateTimer) { + Script.clearInterval(sendAssetParticleEffectUpdateTimer); + sendAssetParticleEffectUpdateTimer = null; + } + if (sendAssetParticleEffect) { + sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect); + } + sendAssetRecipient = null; +} + +var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); +var UI_FADE_TIMEOUT_MS = 150; +function maybeEnableHMDPreview() { + // Set a small timeout to prevent sensitive data from being shown during UI fade + Script.setTimeout(function () { + setTabletVisibleInSecondaryCamera(true); + DesktopPreviewProvider.setPreviewDisabledReason("USER"); + Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption); + }, UI_FADE_TIMEOUT_MS); +} - if (tablet) { - tablet.screenChanged.disconnect(onTabletScreenChanged); - if (onMarketplaceScreen) { - tablet.gotoHomeScreen(); +var onQmlMessageReceived = function onQmlMessageReceived(message) { + if (message.messageSrc === "HTML") { + return; + } + switch (message.method) { + case 'purchases_openWallet': + case 'checkout_openWallet': + case 'checkout_setUpClicked': + openWallet(); + break; + case 'purchases_walletNotSetUp': + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: "purchases" + }); + openWallet(); + break; + case 'checkout_walletNotSetUp': + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: message.referrer === "itemPage" ? message.itemId : message.referrer + }); + openWallet(); + break; + case 'checkout_cancelClicked': + ui.open(MARKETPLACE_URL + '/items/' + message.params, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'header_goToPurchases': + case 'checkout_goToPurchases': + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = message.filterText; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + break; + case 'checkout_itemLinkClicked': + ui.open(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'checkout_continueShopping': + ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'purchases_itemInfoClicked': + var itemId = message.itemId; + if (itemId && itemId !== "") { + ui.open(MARKETPLACE_URL + '/items/' + itemId, MARKETPLACES_INJECT_SCRIPT_URL); + } + break; + case 'checkout_rezClicked': + case 'purchases_rezClicked': + rezEntity(message.itemHref, message.itemType); + break; + case 'header_marketplaceImageClicked': + case 'purchases_backClicked': + ui.open(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'purchases_goToMarketplaceClicked': + ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'updateItemClicked': + ui.open(message.upgradeUrl + "?edition=" + message.itemEdition, + MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'giftAsset': + + break; + case 'passphrasePopup_cancelClicked': + case 'needsLogIn_cancelClicked': + ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'needsLogIn_loginClicked': + openLoginWindow(); + break; + case 'disableHmdPreview': + if (!savedDisablePreviewOption) { + savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); + } + + if (!savedDisablePreviewOption) { + DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); + Menu.setIsOptionChecked("Disable Preview", true); + setTabletVisibleInSecondaryCamera(false); + } + break; + case 'maybeEnableHmdPreview': + maybeEnableHMDPreview(); + break; + case 'purchases_openGoTo': + ui.open("hifi/tablet/TabletAddressDialog.qml"); + break; + case 'purchases_itemCertificateClicked': + contextOverlayEntity = ""; + setCertificateInfo(contextOverlayEntity, message.itemCertificateId); + break; + case 'inspectionCertificate_closeClicked': + ui.close(); + break; + case 'inspectionCertificate_requestOwnershipVerification': + ContextOverlay.requestOwnershipVerification(message.entity); + break; + case 'inspectionCertificate_showInMarketplaceClicked': + ui.open(message.marketplaceUrl, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'header_myItemsClicked': + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'purchases_showMyItems' + }); + break; + case 'refreshConnections': + // Guard to prevent this code from being executed while sending money -- + // we only want to execute this while sending non-HFC gifts + if (!onWalletScreen) { + print('Refreshing Connections...'); + getConnectionData(false); + } + break; + case 'enable_ChooseRecipientNearbyMode': + // Guard to prevent this code from being executed while sending money -- + // we only want to execute this while sending non-HFC gifts + if (!onWalletScreen) { + if (!isUpdateOverlaysWired) { + Script.update.connect(updateOverlays); + isUpdateOverlaysWired = true; + } + } + break; + case 'disable_ChooseRecipientNearbyMode': + // Guard to prevent this code from being executed while sending money -- + // we only want to execute this while sending non-HFC gifts + if (!onWalletScreen) { + if (isUpdateOverlaysWired) { + Script.update.disconnect(updateOverlays); + isUpdateOverlaysWired = false; + } + removeOverlays(); + } + break; + case 'wallet_availableUpdatesReceived': + case 'purchases_availableUpdatesReceived': + userHasUpdates = message.numUpdates > 0; + ui.messagesWaiting(userHasUpdates); + break; + case 'purchases_updateWearables': + var currentlyWornWearables = []; + var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) + + var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); + + for (var i = 0; i < nearbyEntities.length; i++) { + var currentProperties = Entities.getEntityProperties( + nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID'] + ); + if (currentProperties.parentID === MyAvatar.sessionUUID) { + currentlyWornWearables.push({ + entityID: nearbyEntities[i], + entityCertID: currentProperties.certificateID, + entityEdition: currentProperties.editionNumber + }); } } + ui.tablet.sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); + break; + case 'sendAsset_sendPublicly': + if (message.assetName !== "") { + deleteSendAssetParticleEffect(); + sendAssetRecipient = message.recipient; + var props = SEND_ASSET_PARTICLE_PROPERTIES; + props.parentID = MyAvatar.sessionUUID; + props.position = MyAvatar.position; + props.position.y += 0.2; + if (message.effectImage) { + props.textures = message.effectImage; + } + sendAssetParticleEffect = Entities.addEntity(props, true); + particleEffectTimestamp = Date.now(); + updateSendAssetParticleEffect(); + sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, + SEND_ASSET_PARTICLE_TIMER_UPDATE); + } + break; + case 'http.request': + // Handled elsewhere, don't log. + break; + case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? + break; + default: + print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); + } +}; + +// Function Name: onTabletScreenChanged() +// +// Description: +// -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string +// value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. +var onMarketplaceScreen = false; +var onWalletScreen = false; +var onCommerceScreen = false; +var onInspectionCertificateScreen = false; +var onTabletScreenChanged = function onTabletScreenChanged(type, url) { + ui.setCurrentVisibleScreenMetadata(type, url); + onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; + onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; + var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; + var onCommerceScreenNow = type === "QML" && + (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || + onInspectionCertificateScreen); + + // exiting wallet or commerce screen + if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) { + maybeEnableHMDPreview(); + } + + onCommerceScreen = onCommerceScreenNow; + onWalletScreen = onWalletScreenNow; + wireQmlEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); + + if (url === MARKETPLACE_PURCHASES_QML_PATH) { + ui.tablet.sendToQml({ + method: 'updatePurchases', + referrerURL: referrerURL, + filterText: filterText + }); + } + + ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; + ui.buttonActive(ui.isOpen); + + if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { + ContextOverlay.isInMarketplaceInspectionMode = true; + } else { + ContextOverlay.isInMarketplaceInspectionMode = false; + } + + if (onInspectionCertificateScreen) { + setCertificateInfo(contextOverlayEntity); + } + + if (onCommerceScreen) { + if (!isWired) { + Users.usernameFromIDReply.connect(usernameFromIDReply); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + } + isWired = true; + Wallet.refreshWalletStatus(); + } else { + ui.tablet.sendToQml({ + method: 'inspectionCertificate_resetCert' + }); off(); } + console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + + "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); +}; - // - // Run the functions. - // - startup(); - Script.scriptEnding.connect(shutdown); +// +// Manage the connection between the button and the window. +// +var BUTTON_NAME = "MARKET"; +var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; +var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. +var ui; +function startup() { + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 9, + inject: MARKETPLACES_INJECT_SCRIPT_URL, + home: MARKETPLACE_URL_INITIAL, + onScreenChanged: onTabletScreenChanged, + onMessage: onQmlMessageReceived + }); + ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); + Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); + ui.tablet.webEventReceived.connect(onWebEventReceived); + Wallet.walletStatusChanged.connect(sendCommerceSettings); + Window.messageBoxClosed.connect(onMessageBoxClosed); + Wallet.refreshWalletStatus(); +} + +var isWired = false; +var isUpdateOverlaysWired = false; +function off() { + if (isWired) { + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + triggerMapping.disable(); + triggerPressMapping.disable(); + + isWired = false; + } + + if (isUpdateOverlaysWired) { + Script.update.disconnect(updateOverlays); + isUpdateOverlaysWired = false; + } + removeOverlays(); +} +function shutdown() { + maybeEnableHMDPreview(); + deleteSendAssetParticleEffect(); + + Window.messageBoxClosed.disconnect(onMessageBoxClosed); + Wallet.walletStatusChanged.disconnect(sendCommerceSettings); + ui.tablet.webEventReceived.disconnect(onWebEventReceived); + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); + Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); + ContextOverlay.contextOverlayClicked.disconnect(openInspectionCertificateQML); + + off(); +} + +// +// Run the functions. +// +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ebb45130e5..85898c28fb 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,9 @@ "use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/* jslint vars:true, plusplus:true, forin:true */ +/* global Tablet, Settings, Script, AvatarList, Users, Entities, + MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, + UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -20,7 +23,7 @@ var AppUi = Script.require('appUi'); var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, off, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - CHANNEL, getConnectionData, findableByChanged, + CHANNEL, getConnectionData, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -318,6 +321,10 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See break; case 'http.request': break; // Handled by request-service. + case 'hideNotificationDot': + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -361,8 +368,8 @@ function getProfilePicture(username, callback) { // callback(url) if successfull }); } var SAFETY_LIMIT = 400; -function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - var url = METAVERSE_BASE + '/api/v1/users?per_page=' + SAFETY_LIMIT + '&'; +function getAvailableConnections(domain, callback, numResultsPerPage) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + var url = METAVERSE_BASE + '/api/v1/users?per_page=' + (numResultsPerPage || SAFETY_LIMIT) + '&'; if (domain) { url += 'status=' + domain.slice(1, -1); // without curly braces } else { @@ -713,7 +720,7 @@ function tabletVisibilityChanged() { if (!ui.tablet.tabletShown && ui.isOpen) { ui.close(); } - } +} var UPDATE_INTERVAL_MS = 100; var updateInterval; @@ -725,10 +732,14 @@ function createUpdateInterval() { var previousContextOverlay = ContextOverlay.enabled; var previousRequestsDomainListData = Users.requestsDomainListData; -function on() { +function palOpened() { + ui.sendMessage({ + method: 'changeConnectionsDotStatus', + shouldShowDot: shouldShowDot + }); previousContextOverlay = ContextOverlay.enabled; - previousRequestsDomainListData = Users.requestsDomainListData + previousRequestsDomainListData = Users.requestsDomainListData; ContextOverlay.enabled = false; Users.requestsDomainListData = true; @@ -807,14 +818,98 @@ function avatarSessionChanged(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } +function notificationDataProcessPage(data) { + return data.data.users; +} + +var shouldShowDot = false; +var storedOnlineUsersArray = []; +function notificationPollCallback(connectionsArray) { + // + // START logic for handling online/offline user changes + // + var i, j; + var newlyOnlineConnectionsArray = []; + for (i = 0; i < connectionsArray.length; i++) { + var currentUser = connectionsArray[i]; + + if (connectionsArray[i].online) { + var indexOfStoredOnlineUser = -1; + for (j = 0; j < storedOnlineUsersArray.length; j++) { + if (currentUser.username === storedOnlineUsersArray[j].username) { + indexOfStoredOnlineUser = j; + break; + } + } + // If the user record isn't already presesnt inside `storedOnlineUsersArray`... + if (indexOfStoredOnlineUser < 0) { + storedOnlineUsersArray.push(currentUser); + newlyOnlineConnectionsArray.push(currentUser); + } + } else { + var indexOfOfflineUser = -1; + for (j = 0; j < storedOnlineUsersArray.length; j++) { + if (currentUser.username === storedOnlineUsersArray[j].username) { + indexOfOfflineUser = j; + break; + } + } + if (indexOfOfflineUser >= 0) { + storedOnlineUsersArray.splice(indexOfOfflineUser); + } + } + } + // If there's new data, the light should turn on. + // If the light is already on and you have connections online, the light should stay on. + // In all other cases, the light should turn off or stay off. + shouldShowDot = newlyOnlineConnectionsArray.length > 0 || (storedOnlineUsersArray.length > 0 && shouldShowDot); + // + // END logic for handling online/offline user changes + // + + if (!ui.isOpen) { + ui.messagesWaiting(shouldShowDot); + ui.sendMessage({ + method: 'changeConnectionsDotStatus', + shouldShowDot: shouldShowDot + }); + + if (newlyOnlineConnectionsArray.length > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = newlyOnlineConnectionsArray.length + " of your connections " + + (newlyOnlineConnectionsArray.length === 1 ? "is" : "are") + " online. Open PEOPLE to join them!"; + ui.notificationDisplayBanner(message); + } else { + for (i = 0; i < newlyOnlineConnectionsArray.length; i++) { + message = newlyOnlineConnectionsArray[i].username + " is available in " + + newlyOnlineConnectionsArray[i].location.root.name + ". Open PEOPLE to join them!"; + ui.notificationDisplayBanner(message); + } + } + } + } +} + +function isReturnedDataEmpty(data) { + var usersArray = data.data.users; + return usersArray.length === 0; +} + function startup() { ui = new AppUi({ buttonName: "PEOPLE", sortOrder: 7, home: "hifi/Pal.qml", - onOpened: on, + onOpened: palOpened, onClosed: off, - onMessage: fromQml + onMessage: fromQml, + notificationPollEndpoint: "/api/v1/users?filter=connections&per_page=10", + notificationPollTimeoutMs: 60000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: false }); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 37270f896e..3d744b3bd2 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,28 +7,19 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ +/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, + OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function () { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/accountUtils.js"); +var AppUi = Script.require('appUi'); var SNAPSHOT_DELAY = 500; // 500ms var FINISH_SOUND_DELAY = 350; var resetOverlays; var reticleVisible; -var buttonName = "SNAP"; -var buttonConnected = false; - -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -var button = tablet.addButton({ - icon: "icons/tablet-icons/snap-i.svg", - activeIcon: "icons/tablet-icons/snap-a.svg", - text: buttonName, - sortOrder: 5 -}); - var snapshotOptions = {}; var imageData = []; var storyIDsToMaybeDelete = []; @@ -52,8 +43,6 @@ try { print('Failed to resolve request api, error: ' + err); } - - function removeFromStoryIDsToMaybeDelete(story_id) { storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(story_id), 1); print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); @@ -73,33 +62,32 @@ function onMessage(message) { // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the // same time, show the user all of them, and have the user deselect any that they do not want to share. // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. - message = JSON.parse(message); if (message.type !== "snapshot") { return; } switch (message.action) { case 'ready': // DOM is ready and page has loaded - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "captureSettings", setting: Settings.getValue("alsoTakeAnimatedSnapshot", true) - })); + }); if (Snapshot.getSnapshotsLocation() !== "") { isDomainOpen(Settings.getValue("previousSnapshotDomainID"), function (canShare) { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showPreviousImages", options: snapshotOptions, image_data: imageData, canShare: canShare - })); + }); }); } else { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showSetupInstructions" - })); + }); Settings.setValue("previousStillSnapPath", ""); Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); @@ -124,7 +112,7 @@ function onMessage(message) { || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); } else { - tablet.loadQMLOnTop("hifi/tablet/TabletGeneralPreferences.qml"); + ui.openNewAppOnTop("hifi/tablet/TabletGeneralPreferences.qml"); } break; case 'captureStillAndGif': @@ -284,7 +272,6 @@ var POLAROID_RATE_LIMIT_MS = 1000; var polaroidPrintingIsRateLimited = false; function printToPolaroid(image_url) { - // Rate-limit printing if (polaroidPrintingIsRateLimited) { return; @@ -376,19 +363,6 @@ function fillImageDataFromPrevious() { } } -var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); -var isInSnapshotReview = false; -function onButtonClicked() { - if (isInSnapshotReview){ - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - fillImageDataFromPrevious(); - tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); - HMD.openTablet(); - } -} - function snapshotUploaded(isError, reply) { if (!isError) { var replyJson = JSON.parse(reply), @@ -409,12 +383,12 @@ function snapshotUploaded(isError, reply) { } if ((isGif && !ignoreGifSnapshotData) || (!isGif && !ignoreStillSnapshotData)) { print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotUploadComplete", story_id: storyID, image_url: imageURL, - })); + }); if (isGif) { Settings.setValue("previousAnimatedSnapStoryID", storyID); } else { @@ -429,10 +403,10 @@ function snapshotUploaded(isError, reply) { } var href, snapshotDomainID; function takeSnapshot() { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "clearPreviousImages" - })); + }); Settings.setValue("previousStillSnapPath", ""); Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); @@ -471,10 +445,6 @@ function takeSnapshot() { } else { Window.stillSnapshotTaken.connect(stillSnapshotTaken); } - if (buttonConnected) { - button.clicked.disconnect(onButtonClicked); - buttonConnected = false; - } // hide overlays if they are on if (resetOverlays) { @@ -538,10 +508,6 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { Menu.setIsOptionChecked("Show Overlays", true); } Window.stillSnapshotTaken.disconnect(stillSnapshotTaken); - if (!buttonConnected) { - button.clicked.connect(onButtonClicked); - buttonConnected = true; - } // A Snapshot Review dialog might be left open indefinitely after taking the picture, // during which time the user may have moved. So stash that info in the dialog so that @@ -559,12 +525,12 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { isLoggedIn: isLoggedIn }; imageData = [{ localPath: pathStillSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } @@ -572,10 +538,10 @@ function snapshotDirChanged(snapshotPath) { Window.browseDirChanged.disconnect(snapshotDirChanged); if (snapshotPath !== "") { // not cancelled Snapshot.setSnapshotsLocation(snapshotPath); - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotLocationChosen" - })); + }); } } @@ -603,22 +569,18 @@ function processingGifStarted(pathStillSnapshot) { isLoggedIn: isLoggedIn }; imageData = [{ localPath: pathStillSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } function processingGifCompleted(pathAnimatedSnapshot) { isLoggedIn = Account.isLoggedIn(); Window.processingGifCompleted.disconnect(processingGifCompleted); - if (!buttonConnected) { - button.clicked.connect(onButtonClicked); - buttonConnected = true; - } Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot); @@ -631,12 +593,12 @@ function processingGifCompleted(pathAnimatedSnapshot) { canBlast: location.domainID === Settings.getValue("previousSnapshotDomainID"), }; imageData = [{ localPath: pathAnimatedSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } function maybeDeleteSnapshotStories() { @@ -655,28 +617,16 @@ function maybeDeleteSnapshotStories() { }); storyIDsToMaybeDelete = []; } -function onTabletScreenChanged(type, url) { - var wasInSnapshotReview = isInSnapshotReview; - isInSnapshotReview = (type === "Web" && url === SNAPSHOT_REVIEW_URL); - button.editProperties({ isActive: isInSnapshotReview }); - if (isInSnapshotReview !== wasInSnapshotReview) { - if (isInSnapshotReview) { - tablet.webEventReceived.connect(onMessage); - } else { - tablet.webEventReceived.disconnect(onMessage); - } - } -} function onUsernameChanged() { fillImageDataFromPrevious(); isDomainOpen(Settings.getValue("previousSnapshotDomainID"), function (canShare) { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showPreviousImages", options: snapshotOptions, image_data: imageData, canShare: canShare - })); + }); }); if (isLoggedIn) { if (shareAfterLogin) { @@ -705,10 +655,10 @@ function onUsernameChanged() { function snapshotLocationSet(location) { if (location !== "") { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotLocationChosen" - })); + }); } } @@ -733,36 +683,36 @@ function processRezPermissionChange(canRez) { action = 'setPrintButtonDisabled'; } - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action : action - })); + }); } -button.clicked.connect(onButtonClicked); -buttonConnected = true; +function startup() { + ui = new AppUi({ + buttonName: "SNAP", + sortOrder: 5, + home: Script.resolvePath("html/SnapshotReview.html"), + onOpened: fillImageDataFromPrevious, + onMessage: onMessage + }); -Window.snapshotShared.connect(snapshotUploaded); -tablet.screenChanged.connect(onTabletScreenChanged); -GlobalServices.myUsernameChanged.connect(onUsernameChanged); -Snapshot.snapshotLocationSet.connect(snapshotLocationSet); + Entities.canRezChanged.connect(updatePrintPermissions); + Entities.canRezTmpChanged.connect(updatePrintPermissions); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); + Snapshot.snapshotLocationSet.connect(snapshotLocationSet); + Window.snapshotShared.connect(snapshotUploaded); +} +startup(); -Entities.canRezChanged.connect(updatePrintPermissions); -Entities.canRezTmpChanged.connect(updatePrintPermissions); - -Script.scriptEnding.connect(function () { - if (buttonConnected) { - button.clicked.disconnect(onButtonClicked); - buttonConnected = false; - } - if (tablet) { - tablet.removeButton(button); - tablet.screenChanged.disconnect(onTabletScreenChanged); - } +function shutdown() { Window.snapshotShared.disconnect(snapshotUploaded); Snapshot.snapshotLocationSet.disconnect(snapshotLocationSet); + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); Entities.canRezChanged.disconnect(updatePrintPermissions); Entities.canRezTmpChanged.disconnect(updatePrintPermissions); -}); +} +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 8fafac7685..804f838d04 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -1,9 +1,10 @@ "use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ -/*global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print*/ +/* jslint vars:true, plusplus:true, forin:true */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +/* global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print */ // -// goto.js +// tablet-goto.js // scripts/system/ // // Created by Dante Ruiz on 8 February 2017 @@ -14,148 +15,118 @@ // (function () { // BEGIN LOCAL_SCOPE +var request = Script.require('request').request; +var AppUi = Script.require('appUi'); +var DEBUG = false; +function debug() { + if (!DEBUG) { + return; + } + print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); +} - var request = Script.require('request').request; - var DEBUG = false; - function debug() { - if (!DEBUG) { +var stories = {}, pingPong = false; +function expire(id) { + var options = { + uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, + method: 'PUT', + json: true, + body: {expire: "true"} + }; + request(options, function (error, response) { + debug('expired story', options, 'error:', error, 'response:', response); + if (error || (response.status !== 'success')) { + print("ERROR expiring story: ", error || response.status); + } + }); +} +var PER_PAGE_DEBUG = 10; +var PER_PAGE_NORMAL = 100; +function pollForAnnouncements() { + // We could bail now if !Account.isLoggedIn(), but what if we someday have system-wide announcments? + var actions = 'announcement'; + var count = DEBUG ? PER_PAGE_DEBUG : PER_PAGE_NORMAL; + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()), + 'per_page=' + count + ]; + var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); + request({ + uri: url + }, function (error, data) { + debug(url, error, data); + if (error || (data.status !== 'success')) { + print("Error: unable to get", url, error || data.status); return; } - print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); - } - - var gotoQmlSource = "hifi/tablet/TabletAddressDialog.qml"; - var buttonName = "GOTO"; - var onGotoScreen = false; - var shouldActivateButton = false; - function ignore() { } - - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var NORMAL_ICON = "icons/tablet-icons/goto-i.svg"; - var NORMAL_ACTIVE = "icons/tablet-icons/goto-a.svg"; - var WAITING_ICON = "icons/tablet-icons/goto-msg.svg"; - var button = tablet.addButton({ - icon: NORMAL_ICON, - activeIcon: NORMAL_ACTIVE, - text: buttonName, - sortOrder: 8 - }); - - function messagesWaiting(isWaiting) { - button.editProperties({ - icon: isWaiting ? WAITING_ICON : NORMAL_ICON - // No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway. - }); - } - - function onClicked() { - if (onGotoScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - shouldActivateButton = true; - tablet.loadQMLSource(gotoQmlSource); - onGotoScreen = true; - } - } - - function onScreenChanged(type, url) { - ignore(type); - if (url === gotoQmlSource) { - onGotoScreen = true; - shouldActivateButton = true; - button.editProperties({isActive: shouldActivateButton}); - messagesWaiting(false); - } else { - shouldActivateButton = false; - onGotoScreen = false; - button.editProperties({isActive: shouldActivateButton}); - } - } - button.clicked.connect(onClicked); - tablet.screenChanged.connect(onScreenChanged); - - var stories = {}, pingPong = false; - function expire(id) { - var options = { - uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, - method: 'PUT', - json: true, - body: {expire: "true"} - }; - request(options, function (error, response) { - debug('expired story', options, 'error:', error, 'response:', response); - if (error || (response.status !== 'success')) { - print("ERROR expiring story: ", error || response.status); + var didNotify = false, key; + pingPong = !pingPong; + data.user_stories.forEach(function (story) { + var stored = stories[story.id], storedOrNew = stored || story; + debug('story exists:', !!stored, storedOrNew); + if ((storedOrNew.username === Account.username) && (storedOrNew.place_name !== location.placename)) { + if (storedOrNew.audience === 'for_connections') { // Only expire if we haven't already done so. + expire(story.id); + } + return; // before marking } - }); - } - function pollForAnnouncements() { - // We could bail now if !Account.isLoggedIn(), but what if we someday have system-wide announcments? - var actions = 'announcement'; - var count = DEBUG ? 10 : 100; - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(Window.protocolSignature()), - 'per_page=' + count - ]; - var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); - request({ - uri: url - }, function (error, data) { - debug(url, error, data); - if (error || (data.status !== 'success')) { - print("Error: unable to get", url, error || data.status); + storedOrNew.pingPong = pingPong; + if (stored) { // already seen return; } - var didNotify = false, key; - pingPong = !pingPong; - data.user_stories.forEach(function (story) { - var stored = stories[story.id], storedOrNew = stored || story; - debug('story exists:', !!stored, storedOrNew); - if ((storedOrNew.username === Account.username) && (storedOrNew.place_name !== location.placename)) { - if (storedOrNew.audience == 'for_connections') { // Only expire if we haven't already done so. - expire(story.id); - } - return; // before marking - } - storedOrNew.pingPong = pingPong; - if (stored) { // already seen - return; - } - stories[story.id] = story; - var message = story.username + " " + story.action_string + " in " + story.place_name + ". Open GOTO to join them."; - Window.displayAnnouncement(message); - didNotify = true; - }); - for (key in stories) { // Any story we were tracking that was not marked, has expired. - if (stories[key].pingPong !== pingPong) { - debug('removing story', key); - delete stories[key]; - } - } - if (didNotify) { - messagesWaiting(true); - if (HMD.isHandControllerAvailable()) { - var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands - Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); - } - } else if (!Object.keys(stories).length) { // If there's nothing being tracked, then any messageWaiting has expired. - messagesWaiting(false); - } + stories[story.id] = story; + var message = story.username + " " + story.action_string + " in " + + story.place_name + ". Open GOTO to join them."; + Window.displayAnnouncement(message); + didNotify = true; }); - } - var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? 10 : 60) * 1000; - var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); - - Script.scriptEnding.connect(function () { - Script.clearInterval(pollTimer); - button.clicked.disconnect(onClicked); - tablet.removeButton(button); - tablet.screenChanged.disconnect(onScreenChanged); + for (key in stories) { // Any story we were tracking that was not marked, has expired. + if (stories[key].pingPong !== pingPong) { + debug('removing story', key); + delete stories[key]; + } + } + if (didNotify) { + ui.messagesWaiting(true); + if (HMD.isHandControllerAvailable()) { + var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands + Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); + } + } else if (!Object.keys(stories).length) { // If there's nothing being tracked, then any messageWaiting has expired. + ui.messagesWaiting(false); + } }); +} +var MS_PER_SEC = 1000; +var DEBUG_POLL_TIME_SEC = 10; +var NORMAL_POLL_TIME_SEC = 60; +var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? DEBUG_POLL_TIME_SEC : NORMAL_POLL_TIME_SEC) * MS_PER_SEC; +var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); +function gotoOpened() { + ui.messagesWaiting(false); +} + +var ui; +var GOTO_QML_SOURCE = "hifi/tablet/TabletAddressDialog.qml"; +var BUTTON_NAME = "GOTO"; +function startup() { + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 8, + onOpened: gotoOpened, + home: GOTO_QML_SOURCE + }); +} + +function shutdown() { + Script.clearInterval(pollTimer); +} + +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE