Merge pull request #13624 from howard-stearns/app-refactor4-merge

appUi module, and use in People app
This commit is contained in:
Howard Stearns 2018-07-19 11:07:54 -07:00 committed by GitHub
commit 2378cdfdc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 243 additions and 108 deletions

187
scripts/modules/appUi.js Normal file
View file

@ -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;

View file

@ -12,14 +12,15 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // 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, var populateNearbyUserList, color, textures, removeOverlays,
controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, controllerComputePickRay, off,
receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL,
tablet, CHANNEL, getConnectionData, findableByChanged, CHANNEL, getConnectionData, findableByChanged,
avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; avatarAdded, avatarRemoved, avatarSessionChanged; // forward references;
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // 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 UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6};
var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29};
var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now
var METAVERSE_BASE = Account.metaverseServerURL;
Script.include("/~/system/libraries/controllers.js"); Script.include("/~/system/libraries/controllers.js");
@ -221,7 +223,7 @@ function convertDbToLinear(decibels) {
return Math.pow(2, decibels / 10.0); return Math.pow(2, decibels / 10.0);
} }
function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
var data; var data, connectionUserName, friendUserName;
switch (message.method) { switch (message.method) {
case 'selected': case 'selected':
selectedIds = message.params; selectedIds = message.params;
@ -281,7 +283,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
} }
getConnectionData(false); getConnectionData(false);
}); });
break break;
case 'removeFriend': case 'removeFriend':
friendUserName = message.params; friendUserName = message.params;
@ -296,7 +298,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
} }
getConnectionData(friendUserName); getConnectionData(friendUserName);
}); });
break break;
case 'addFriend': case 'addFriend':
friendUserName = message.params; friendUserName = message.params;
print("Adding " + friendUserName + " to friends."); print("Adding " + friendUserName + " to friends.");
@ -307,24 +309,23 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
body: { body: {
username: friendUserName, username: friendUserName,
} }
}, function (error, response) { }, function (error, response) {
if (error || (response.status !== 'success')) { if (error || (response.status !== 'success')) {
print("Error: unable to friend " + friendUserName, error || response.status); print("Error: unable to friend " + friendUserName, error || response.status);
return; return;
}
getConnectionData(friendUserName);
} }
); getConnectionData(friendUserName);
});
break; break;
case 'http.request': case 'http.request':
break; // Handled by request-service. break; // Handled by request-service.
default: default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message)); print('Unrecognized message from Pal.qml:', JSON.stringify(message));
} }
} }
function sendToQml(message) { function sendToQml(message) {
tablet.sendToQml(message); ui.sendMessage(message);
} }
function updateUser(data) { function updateUser(data) {
print('PAL update:', JSON.stringify(data)); print('PAL update:', JSON.stringify(data));
@ -334,7 +335,6 @@ function updateUser(data) {
// User management services // User management services
// //
// These are prototype versions that will be changed when the back end changes. // 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. function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise.
request({ request({
@ -362,7 +362,7 @@ function getProfilePicture(username, callback) { // callback(url) if successfull
}); });
} }
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) 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) { if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces url += 'status=' + domain.slice(1, -1); // without curly braces
} else { } else {
@ -373,7 +373,7 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca
}); });
} }
function getInfoAboutUser(specificUsername, callback) { 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) { requestJSON(url, function (connectionsData) {
for (user in connectionsData.users) { for (user in connectionsData.users) {
if (connectionsData.users[user].username === specificUsername) { 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.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); 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() { function tabletVisibilityChanged() {
if (!tablet.tabletShown && onPalScreen) { if (!ui.tablet.tabletShown && ui.isOpen) {
ContextOverlay.enabled = true; ui.close();
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;
}
} }
} }
var UPDATE_INTERVAL_MS = 100; var UPDATE_INTERVAL_MS = 100;
var updateInterval;
function createUpdateInterval() { function createUpdateInterval() {
return Script.setInterval(function () { return Script.setInterval(function () {
updateOverlays(); updateOverlays();
}, UPDATE_INTERVAL_MS); }, UPDATE_INTERVAL_MS);
} }
function onTabletScreenChanged(type, url) { var previousContextOverlay = ContextOverlay.enabled;
wasOnPalScreen = onPalScreen; var previousRequestsDomainListData = Users.requestsDomainListData;
onPalScreen = (type === "QML" && url === PAL_QML_SOURCE); function on() {
wireEventBridge(onPalScreen);
// for toolbar mode: change button to active when window is first openend, false otherwise.
button.editProperties({isActive: onPalScreen});
if (onPalScreen) { previousContextOverlay = ContextOverlay.enabled;
isWired = true; previousRequestsDomainListData = Users.requestsDomainListData
ContextOverlay.enabled = false;
Users.requestsDomainListData = true;
ContextOverlay.enabled = false; ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged);
Users.requestsDomainListData = true; updateInterval = createUpdateInterval();
Controller.mousePressEvent.connect(handleMouseEvent);
tablet.tabletShownChanged.connect(tabletVisibilityChanged); Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
updateInterval = createUpdateInterval(); Users.usernameFromIDReply.connect(usernameFromIDReply);
Controller.mousePressEvent.connect(handleMouseEvent); triggerMapping.enable();
Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerPressMapping.enable();
Users.usernameFromIDReply.connect(usernameFromIDReply); populateNearbyUserList();
triggerMapping.enable();
triggerPressMapping.enable();
populateNearbyUserList();
} else {
off();
if (wasOnPalScreen) {
ContextOverlay.enabled = true;
}
}
} }
// //
@ -789,8 +753,8 @@ function receiveMessage(channel, messageString, senderID) {
var message = JSON.parse(messageString); var message = JSON.parse(messageString);
switch (message.method) { switch (message.method) {
case 'select': case 'select':
if (!onPalScreen) { if (!ui.isOpen) {
tablet.loadQMLSource(PAL_QML_SOURCE); ui.open();
Script.setTimeout(function () { sendToQml(message); }, 1000); Script.setTimeout(function () { sendToQml(message); }, 1000);
} else { } else {
sendToQml(message); // Accepts objects, not just strings. sendToQml(message); // Accepts objects, not just strings.
@ -826,9 +790,8 @@ function avatarDisconnected(nodeID) {
function clearLocalQMLDataAndClosePAL() { function clearLocalQMLDataAndClosePAL() {
sendToQml({ method: 'clearLocalQMLData' }); sendToQml({ method: 'clearLocalQMLData' });
if (onPalScreen) { if (ui.isOpen) {
ContextOverlay.enabled = true; ui.close();
tablet.gotoHomeScreen();
} }
} }
@ -844,20 +807,15 @@ function avatarSessionChanged(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] });
} }
var button;
var buttonName = "PEOPLE";
var tablet = null;
function startup() { function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); ui = new AppUi({
button = tablet.addButton({ buttonName: "PEOPLE",
text: buttonName, sortOrder: 7,
icon: "icons/tablet-icons/people-i.svg", home: "hifi/Pal.qml",
activeIcon: "icons/tablet-icons/people-a.svg", onOpened: on,
sortOrder: 7 onClosed: off,
onMessage: fromQml
}); });
button.clicked.connect(onTabletButtonClicked);
tablet.screenChanged.connect(onTabletScreenChanged);
Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL); Messages.subscribe(CHANNEL);
@ -869,35 +827,25 @@ function startup() {
} }
startup(); startup();
var isWired = false;
function off() { function off() {
if (isWired) { if (ui.isOpen) { // i.e., only when connected
if (updateInterval) { if (updateInterval) {
Script.clearInterval(updateInterval); Script.clearInterval(updateInterval);
} }
Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
Users.usernameFromIDReply.disconnect(usernameFromIDReply); Users.usernameFromIDReply.disconnect(usernameFromIDReply);
ContextOverlay.enabled = true
triggerMapping.disable(); triggerMapping.disable();
triggerPressMapping.disable(); triggerPressMapping.disable();
Users.requestsDomainListData = false;
isWired = false;
} }
removeOverlays(); removeOverlays();
ContextOverlay.enabled = previousContextOverlay;
Users.requestsDomainListData = previousRequestsDomainListData;
} }
function shutdown() { function shutdown() {
if (onPalScreen) {
tablet.gotoHomeScreen();
}
button.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(button);
tablet.screenChanged.disconnect(onTabletScreenChanged);
Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL); Messages.subscribe(CHANNEL);