diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js new file mode 100644 index 0000000000..db81af3755 --- /dev/null +++ b/scripts/modules/appUi.js @@ -0,0 +1,187 @@ +"use strict"; +/*global Tablet, Script*/ +// +// libraries/appUi.js +// +// Created by Howard Stearns on 3/20/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function AppUi(properties) { + /* 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). + 3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"}); + (And if converting an existing app, + define var tablet = ui.tablet, button = ui.button; as needed. + remove button.clicked.[dis]connect and tablet.remove(button).) + 4. Define onOpened and onClosed behavior in #3, if any. + (And if converting an existing app, remove screenChanged.[dis]connect.) + 5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you + want a handler to be "always on", connect it yourself at script startup. + (And if converting an existing app, remove code that [un]wires that message handling such as + fromQml/sendToQml or webEventReceived/emitScriptEvent.) + 6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet, + and use isOpen, open(), and close() as needed.) + 7. lint! + */ + 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 + } + + // Defaults: + that.tabletName = "com.highfidelity.interface.tablet.system"; + that.inject = ""; + that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below. + 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. + }; + 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. + var url = optionalUrl || that.home; + that.setCurrentData(url); + if (that.isQML()) { + that.tablet.loadQMLSource(url); + } else { + that.tablet.gotoWebScreen(url, that.inject); + } + }; + that.close = function close() { // How to close the app. + that.currentUrl = ""; + // 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.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({ + icon: isWaiting ? that.normalMessagesButton : that.normalButton, + activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton + }); + }; + that.isQML = function isQML() { // We set type property in onClick. + return that.type === 'QML'; + }; + that.eventSignal = function eventSignal() { // What signal to hook onMessage to. + return that.isQML() ? that.tablet.fromQml : that.tablet.webEventReceived; + }; + + // Overwrite with the given properties: + Object.keys(properties).forEach(function (key) { that[key] = properties[key]; }); + + // Properties: + that.tablet = Tablet.getTablet(that.tabletName); + // Must be after we gather properties. + that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-"; + defaultButton('normalButton', 'i.svg'); + defaultButton('activeButton', 'a.svg'); + defaultButton('normalMessagesButton', 'i-msg.svg'); + defaultButton('activeMessagesButton', 'a-msg.svg'); + that.button = that.tablet.addButton({ + icon: that.normalButton, + activeIcon: that.activeButton, + text: that.buttonName, + sortOrder: that.sortOrder + }); + 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; + // 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.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(); + // Outbound (always, regardless of whether there is an inbound handler). + if (on) { + that.sendMessage = isQml ? that.tablet.sendToQml : that.sendToHtml; + } else { + that.sendMessage = that.ignore; + } + + 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; + } + } else { + if (that.hasEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.eventSignal()); + that.eventSignal().disconnect(handler); + that.hasEventBridge = false; + } + } + }; + that.isOpen = false; + // To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties. + that.onClicked = that.home + ? function onClicked() { + // Call open() or close(), and reset type based on current home property. + if (that.isOpen) { + that.close(); + } else { + that.open(); + } + } : that.ignore; + that.onScriptEnding = function onScriptEnding() { + // Close if necessary, clean up any remaining handlers, and remove the button. + if (that.isOpen) { + that.close(); + } + that.tablet.screenChanged.disconnect(that.onScreenChanged); + if (that.button) { + if (that.onClicked) { + that.button.clicked.disconnect(that.onClicked); + } + that.tablet.removeButton(that.button); + } + }; + // Set up the handlers. + that.tablet.screenChanged.connect(that.onScreenChanged); + that.button.clicked.connect(that.onClicked); + Script.scriptEnding.connect(that.onScriptEnding); +} +module.exports = AppUi; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index fcff83a3d3..9485b8b49a 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -12,14 +12,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE - var request = Script.require('request').request; +var request = Script.require('request').request; +var AppUi = Script.require('appUi'); var populateNearbyUserList, color, textures, removeOverlays, - controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, + controllerComputePickRay, off, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - tablet, CHANNEL, getConnectionData, findableByChanged, + CHANNEL, getConnectionData, findableByChanged, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -40,6 +41,7 @@ var HOVER_TEXTURES = { 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}; // almost white for now +var METAVERSE_BASE = Account.metaverseServerURL; Script.include("/~/system/libraries/controllers.js"); @@ -221,7 +223,7 @@ function convertDbToLinear(decibels) { return Math.pow(2, decibels / 10.0); } function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. - var data; + var data, connectionUserName, friendUserName; switch (message.method) { case 'selected': selectedIds = message.params; @@ -281,7 +283,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } getConnectionData(false); }); - break + break; case 'removeFriend': friendUserName = message.params; @@ -296,7 +298,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } getConnectionData(friendUserName); }); - break + break; case 'addFriend': friendUserName = message.params; print("Adding " + friendUserName + " to friends."); @@ -307,24 +309,23 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See body: { username: friendUserName, } - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to friend " + friendUserName, error || response.status); - return; - } - getConnectionData(friendUserName); + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to friend " + friendUserName, error || response.status); + return; } - ); + getConnectionData(friendUserName); + }); break; case 'http.request': - break; // Handled by request-service. + break; // Handled by request-service. default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } } function sendToQml(message) { - tablet.sendToQml(message); + ui.sendMessage(message); } function updateUser(data) { print('PAL update:', JSON.stringify(data)); @@ -334,7 +335,6 @@ function updateUser(data) { // User management services // // These are prototype versions that will be changed when the back end changes. -var METAVERSE_BASE = Account.metaverseServerURL; function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. request({ @@ -362,7 +362,7 @@ function getProfilePicture(username, callback) { // callback(url) if successfull }); } function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - url = METAVERSE_BASE + '/api/v1/users?per_page=400&' + var url = METAVERSE_BASE + '/api/v1/users?per_page=400&'; if (domain) { url += 'status=' + domain.slice(1, -1); // without curly braces } else { @@ -373,7 +373,7 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca }); } function getInfoAboutUser(specificUsername, callback) { - url = METAVERSE_BASE + '/api/v1/users?filter=connections' + var url = METAVERSE_BASE + '/api/v1/users?filter=connections'; requestJSON(url, function (connectionsData) { for (user in connectionsData.users) { if (connectionsData.users[user].username === specificUsername) { @@ -705,77 +705,41 @@ triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Cont triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +var ui; +// Most apps can have people toggle the tablet closed and open again, and the app should remain "open" even while +// the tablet is not shown. However, for the pal, we explicitly close the app and return the tablet to it's +// home screen (so that the avatar highlighting goes away). function tabletVisibilityChanged() { - if (!tablet.tabletShown && onPalScreen) { - ContextOverlay.enabled = true; - tablet.gotoHomeScreen(); - } -} - -var wasOnPalScreen = false; -var onPalScreen = false; -var PAL_QML_SOURCE = "hifi/Pal.qml"; -function onTabletButtonClicked() { - if (!tablet) { - print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); - return; - } - if (onPalScreen) { - // In Toolbar Mode, `gotoHomeScreen` will close the app window. - tablet.gotoHomeScreen(); - } else { - tablet.loadQMLSource(PAL_QML_SOURCE); - } -} -var hasEventBridge = false; -function wireEventBridge(on) { - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } + if (!ui.tablet.tabletShown && ui.isOpen) { + ui.close(); } } var UPDATE_INTERVAL_MS = 100; +var updateInterval; function createUpdateInterval() { return Script.setInterval(function () { updateOverlays(); }, UPDATE_INTERVAL_MS); } -function onTabletScreenChanged(type, url) { - wasOnPalScreen = onPalScreen; - onPalScreen = (type === "QML" && url === PAL_QML_SOURCE); - wireEventBridge(onPalScreen); - // for toolbar mode: change button to active when window is first openend, false otherwise. - button.editProperties({isActive: onPalScreen}); +var previousContextOverlay = ContextOverlay.enabled; +var previousRequestsDomainListData = Users.requestsDomainListData; +function on() { - if (onPalScreen) { - isWired = true; + previousContextOverlay = ContextOverlay.enabled; + previousRequestsDomainListData = Users.requestsDomainListData + ContextOverlay.enabled = false; + Users.requestsDomainListData = true; - ContextOverlay.enabled = false; - Users.requestsDomainListData = true; - - tablet.tabletShownChanged.connect(tabletVisibilityChanged); - updateInterval = createUpdateInterval(); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - Users.usernameFromIDReply.connect(usernameFromIDReply); - triggerMapping.enable(); - triggerPressMapping.enable(); - populateNearbyUserList(); - } else { - off(); - if (wasOnPalScreen) { - ContextOverlay.enabled = true; - } - } + ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged); + updateInterval = createUpdateInterval(); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + Users.usernameFromIDReply.connect(usernameFromIDReply); + triggerMapping.enable(); + triggerPressMapping.enable(); + populateNearbyUserList(); } // @@ -789,8 +753,8 @@ function receiveMessage(channel, messageString, senderID) { var message = JSON.parse(messageString); switch (message.method) { case 'select': - if (!onPalScreen) { - tablet.loadQMLSource(PAL_QML_SOURCE); + if (!ui.isOpen) { + ui.open(); Script.setTimeout(function () { sendToQml(message); }, 1000); } else { sendToQml(message); // Accepts objects, not just strings. @@ -826,9 +790,8 @@ function avatarDisconnected(nodeID) { function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); - if (onPalScreen) { - ContextOverlay.enabled = true; - tablet.gotoHomeScreen(); + if (ui.isOpen) { + ui.close(); } } @@ -844,20 +807,15 @@ function avatarSessionChanged(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } - -var button; -var buttonName = "PEOPLE"; -var tablet = null; function startup() { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - text: buttonName, - icon: "icons/tablet-icons/people-i.svg", - activeIcon: "icons/tablet-icons/people-a.svg", - sortOrder: 7 + ui = new AppUi({ + buttonName: "PEOPLE", + sortOrder: 7, + home: "hifi/Pal.qml", + onOpened: on, + onClosed: off, + onMessage: fromQml }); - button.clicked.connect(onTabletButtonClicked); - tablet.screenChanged.connect(onTabletScreenChanged); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); @@ -869,35 +827,25 @@ function startup() { } startup(); - -var isWired = false; function off() { - if (isWired) { + if (ui.isOpen) { // i.e., only when connected if (updateInterval) { Script.clearInterval(updateInterval); } Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); + ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); Users.usernameFromIDReply.disconnect(usernameFromIDReply); - ContextOverlay.enabled = true triggerMapping.disable(); triggerPressMapping.disable(); - Users.requestsDomainListData = false; - - isWired = false; } removeOverlays(); + ContextOverlay.enabled = previousContextOverlay; + Users.requestsDomainListData = previousRequestsDomainListData; } function shutdown() { - if (onPalScreen) { - tablet.gotoHomeScreen(); - } - button.clicked.disconnect(onTabletButtonClicked); - tablet.removeButton(button); - tablet.screenChanged.disconnect(onTabletScreenChanged); Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL);