diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js new file mode 100644 index 0000000000..7930892395 --- /dev/null +++ b/scripts/system/tablet-users.js @@ -0,0 +1,67 @@ +"use strict"; + +// +// users.js +// +// Created by Faye Li on 18 Jan 2017. +// Copyright 2017 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() { // BEGIN LOCAL_SCOPE + var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; + var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; + var FRIENDS_WINDOW_WIDTH = 290; + var FRIENDS_WINDOW_HEIGHT = 500; + var FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/people-i.svg", + text: "Users" + }); + + function onClicked() { + tablet.gotoWebScreen(USERS_URL); + } + + function onWebEventReceived(event) { + print("Script received a web event, its type is " + typeof event); + if (typeof event === "string") { + event = JSON.parse(event); + } + if (event.type === "ready") { + // send username to html + var myUsername = GlobalServices.username; + var object = { + "type": "sendUsername", + "data": {"username": myUsername} + }; + print("sending username: " + myUsername); + tablet.emitScriptEvent(JSON.stringify(object)); + } + if (event.type === "manage-friends") { + // open a web overlay to metaverse friends page + var friendsWindow = new OverlayWebWindow({ + title: FRIENDS_WINDOW_TITLE, + width: FRIENDS_WINDOW_WIDTH, + height: FRIENDS_WINDOW_HEIGHT, + visible: false + }); + friendsWindow.setURL(FRIENDS_WINDOW_URL); + friendsWindow.setVisible(true); + friendsWindow.raise(); + } + } + + button.clicked.connect(onClicked); + tablet.webEventReceived.connect(onWebEventReceived); + + function cleanup() { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + } + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/users.js b/scripts/system/users.js index 7930892395..009c446ff3 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -2,66 +2,1279 @@ // // users.js +// examples // -// Created by Faye Li on 18 Jan 2017. -// Copyright 2017 High Fidelity, Inc. +// Created by David Rowe on 9 Mar 2015. +// Copyright 2015 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 // +/*globals HMD, Toolbars, Script, Menu, Overlays, Tablet, Controller, Settings, OverlayWebWindow, Account, GlobalServices */ (function() { // BEGIN LOCAL_SCOPE - var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; - var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; - var FRIENDS_WINDOW_WIDTH = 290; - var FRIENDS_WINDOW_HEIGHT = 500; - var FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var button = tablet.addButton({ - icon: "icons/tablet-icons/people-i.svg", - text: "Users" - }); +var button; +var buttonName = "USERS"; +var toolBar = null; +var tablet = null; - function onClicked() { - tablet.gotoWebScreen(USERS_URL); +var MENU_ITEM = "Users Online"; + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/people.svg"), + visible: true, + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/users-i.svg", + text: "USERS", + isActive: Menu.isOptionChecked(MENU_ITEM) + }); +} + + +function onClicked() { + Menu.setIsOptionChecked(MENU_ITEM, !Menu.isOptionChecked(MENU_ITEM)); + button.editProperties({isActive: Menu.isOptionChecked(MENU_ITEM)}); +} +button.clicked.connect(onClicked); + +// resolve these paths immediately +var MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); +var BASE_URL = Script.resolvePath("assets/images/tools/"); + +var PopUpMenu = function (properties) { + var value = properties.value, + promptOverlay, + valueOverlay, + buttonOverlay, + optionOverlays = [], + isDisplayingOptions = false, + OPTION_MARGIN = 4, + + MIN_MAX_BUTTON_SVG_WIDTH = 17.1, + MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, + MIN_MAX_BUTTON_WIDTH = 14, + MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH; + + function positionDisplayOptions() { + var y, + i; + + y = properties.y - (properties.values.length - 1) * properties.lineHeight - OPTION_MARGIN; + + for (i = 0; i < properties.values.length; i += 1) { + Overlays.editOverlay(optionOverlays[i], { + y: y + }); + y += properties.lineHeight; + } } - function onWebEventReceived(event) { - print("Script received a web event, its type is " + typeof event); - if (typeof event === "string") { - event = JSON.parse(event); - } - if (event.type === "ready") { - // send username to html - var myUsername = GlobalServices.username; - var object = { - "type": "sendUsername", - "data": {"username": myUsername} - }; - print("sending username: " + myUsername); - tablet.emitScriptEvent(JSON.stringify(object)); - } - if (event.type === "manage-friends") { - // open a web overlay to metaverse friends page - var friendsWindow = new OverlayWebWindow({ - title: FRIENDS_WINDOW_TITLE, - width: FRIENDS_WINDOW_WIDTH, - height: FRIENDS_WINDOW_HEIGHT, - visible: false + function showDisplayOptions() { + var i, + yOffScreen = Controller.getViewportDimensions().y; + + for (i = 0; i < properties.values.length; i += 1) { + optionOverlays[i] = Overlays.addOverlay("text", { + x: properties.x + properties.promptWidth, + y: yOffScreen, + width: properties.width - properties.promptWidth, + height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders + topMargin: OPTION_MARGIN, + leftMargin: OPTION_MARGIN, + color: properties.optionColor, + alpha: properties.optionAlpha, + backgroundColor: properties.popupBackgroundColor, + backgroundAlpha: properties.popupBackgroundAlpha, + text: properties.displayValues[i], + font: properties.font, + visible: true }); + } + + positionDisplayOptions(); + + isDisplayingOptions = true; + } + + function deleteDisplayOptions() { + var i; + + for (i = 0; i < optionOverlays.length; i += 1) { + Overlays.deleteOverlay(optionOverlays[i]); + } + + isDisplayingOptions = false; + } + + function handleClick(overlay) { + var clicked = false, + i; + + if (overlay === valueOverlay || overlay === buttonOverlay) { + showDisplayOptions(); + return true; + } + + if (isDisplayingOptions) { + for (i = 0; i < optionOverlays.length; i += 1) { + if (overlay === optionOverlays[i]) { + value = properties.values[i]; + Overlays.editOverlay(valueOverlay, { + text: properties.displayValues[i] + }); + clicked = true; + } + } + + deleteDisplayOptions(); + } + + return clicked; + } + + function updatePosition(x, y) { + properties.x = x; + properties.y = y; + Overlays.editOverlay(promptOverlay, { + x: x, + y: y + }); + Overlays.editOverlay(valueOverlay, { + x: x + properties.promptWidth, + y: y - OPTION_MARGIN + }); + Overlays.editOverlay(buttonOverlay, { + x: x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, + y: y - OPTION_MARGIN + 1 + }); + if (isDisplayingOptions) { + positionDisplayOptions(); + } + } + + function setVisible(visible) { + Overlays.editOverlay(promptOverlay, { + visible: visible + }); + Overlays.editOverlay(valueOverlay, { + visible: visible + }); + Overlays.editOverlay(buttonOverlay, { + visible: visible + }); + } + + function tearDown() { + Overlays.deleteOverlay(promptOverlay); + Overlays.deleteOverlay(valueOverlay); + Overlays.deleteOverlay(buttonOverlay); + if (isDisplayingOptions) { + deleteDisplayOptions(); + } + } + + function getValue() { + return value; + } + + function setValue(newValue) { + var index; + + index = properties.values.indexOf(newValue); + if (index !== -1) { + value = newValue; + Overlays.editOverlay(valueOverlay, { + text: properties.displayValues[index] + }); + } + } + + promptOverlay = Overlays.addOverlay("text", { + x: properties.x, + y: properties.y, + width: properties.promptWidth, + height: properties.textHeight, + topMargin: 0, + leftMargin: 0, + color: properties.promptColor, + alpha: properties.promptAlpha, + backgroundColor: properties.promptBackgroundColor, + backgroundAlpha: properties.promptBackgroundAlpha, + text: properties.prompt, + font: properties.font, + visible: properties.visible + }); + + valueOverlay = Overlays.addOverlay("text", { + x: properties.x + properties.promptWidth, + y: properties.y, + width: properties.width - properties.promptWidth, + height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders + topMargin: OPTION_MARGIN, + leftMargin: OPTION_MARGIN, + color: properties.optionColor, + alpha: properties.optionAlpha, + backgroundColor: properties.optionBackgroundColor, + backgroundAlpha: properties.optionBackgroundAlpha, + text: properties.displayValues[properties.values.indexOf(value)], + font: properties.font, + visible: properties.visible + }); + + buttonOverlay = Overlays.addOverlay("image", { + x: properties.x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, + y: properties.y, + width: MIN_MAX_BUTTON_WIDTH, + height: MIN_MAX_BUTTON_HEIGHT, + imageURL: MIN_MAX_BUTTON_SVG, + subImage: { + x: 0, + y: 0, + width: MIN_MAX_BUTTON_SVG_WIDTH, + height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 + }, + //color: properties.buttonColor, + alpha: properties.buttonAlpha, + visible: properties.visible + }); + + return { + updatePosition: updatePosition, + setVisible: setVisible, + handleClick: handleClick, + tearDown: tearDown, + getValue: getValue, + setValue: setValue + }; +}; + +var usersWindow = (function () { + + var WINDOW_WIDTH = 260, + WINDOW_MARGIN = 12, + WINDOW_BASE_MARGIN = 24, // A little less is needed in order look correct + WINDOW_FONT = { + size: 12 + }, + WINDOW_FOREGROUND_COLOR = { + red: 240, + green: 240, + blue: 240 + }, + WINDOW_FOREGROUND_ALPHA = 0.95, + WINDOW_HEADING_COLOR = { + red: 180, + green: 180, + blue: 180 + }, + WINDOW_HEADING_ALPHA = 0.95, + WINDOW_BACKGROUND_COLOR = { + red: 80, + green: 80, + blue: 80 + }, + WINDOW_BACKGROUND_ALPHA = 0.8, + windowPane, + windowHeading, + + // Margin on the left and right side of the window to keep + // it from getting too close to the edge of the screen which + // is unclickable. + WINDOW_MARGIN_X = 20, + + // Window border is similar to that of edit.js. + WINDOW_MARGIN_HALF = WINDOW_MARGIN / 2, + WINDOW_BORDER_WIDTH = WINDOW_WIDTH + 2 * WINDOW_MARGIN_HALF, + WINDOW_BORDER_TOP_MARGIN = 2 * WINDOW_MARGIN_HALF, + WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_MARGIN_HALF, + WINDOW_BORDER_LEFT_MARGIN = WINDOW_MARGIN_HALF, + WINDOW_BORDER_RADIUS = 4, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, + WINDOW_BORDER_ALPHA = 0.5, + windowBorder, + + MIN_MAX_BUTTON_SVG = BASE_URL + "min-max-toggle.svg", + MIN_MAX_BUTTON_SVG_WIDTH = 17.1, + MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, + MIN_MAX_BUTTON_WIDTH = 14, + MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH, + MIN_MAX_BUTTON_COLOR = { + red: 255, + green: 255, + blue: 255 + }, + MIN_MAX_BUTTON_ALPHA = 0.9, + minimizeButton, + SCROLLBAR_BACKGROUND_WIDTH = 12, + SCROLLBAR_BACKGROUND_COLOR = { + red: 70, + green: 70, + blue: 70 + }, + SCROLLBAR_BACKGROUND_ALPHA = 0.8, + scrollbarBackground, + SCROLLBAR_BAR_MIN_HEIGHT = 5, + SCROLLBAR_BAR_COLOR = { + red: 170, + green: 170, + blue: 170 + }, + SCROLLBAR_BAR_ALPHA = 0.8, + SCROLLBAR_BAR_SELECTED_ALPHA = 0.95, + scrollbarBar, + scrollbarBackgroundHeight, + scrollbarBarHeight, + FRIENDS_BUTTON_SPACER = 6, // Space before add/remove friends button + FRIENDS_BUTTON_SVG = BASE_URL + "add-remove-friends.svg", + FRIENDS_BUTTON_SVG_WIDTH = 107, + FRIENDS_BUTTON_SVG_HEIGHT = 27, + FRIENDS_BUTTON_WIDTH = FRIENDS_BUTTON_SVG_WIDTH, + FRIENDS_BUTTON_HEIGHT = FRIENDS_BUTTON_SVG_HEIGHT, + FRIENDS_BUTTON_COLOR = { + red: 225, + green: 225, + blue: 225 + }, + FRIENDS_BUTTON_ALPHA = 0.95, + FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends", + FRIENDS_WINDOW_WIDTH = 290, + FRIENDS_WINDOW_HEIGHT = 500, + FRIENDS_WINDOW_TITLE = "Add/Remove Friends", + friendsButton, + friendsWindow, + + OPTION_BACKGROUND_COLOR = { + red: 60, + green: 60, + blue: 60 + }, + OPTION_BACKGROUND_ALPHA = 0.1, + + DISPLAY_SPACER = 12, // Space before display control + DISPLAY_PROMPT = "Show me:", + DISPLAY_PROMPT_WIDTH = 60, + DISPLAY_EVERYONE = "everyone", + DISPLAY_FRIENDS = "friends", + DISPLAY_VALUES = [DISPLAY_EVERYONE, DISPLAY_FRIENDS], + DISPLAY_DISPLAY_VALUES = DISPLAY_VALUES, + DISPLAY_OPTIONS_BACKGROUND_COLOR = { + red: 120, + green: 120, + blue: 120 + }, + DISPLAY_OPTIONS_BACKGROUND_ALPHA = 0.9, + displayControl, + + VISIBILITY_SPACER = 6, // Space before visibility control + VISIBILITY_PROMPT = "Visible to:", + VISIBILITY_PROMPT_WIDTH = 60, + VISIBILITY_ALL = "all", + VISIBILITY_FRIENDS = "friends", + VISIBILITY_NONE = "none", + VISIBILITY_VALUES = [VISIBILITY_ALL, VISIBILITY_FRIENDS, VISIBILITY_NONE], + VISIBILITY_DISPLAY_VALUES = ["everyone", "friends", "no one"], + visibilityControl, + + windowHeight, + windowBorderHeight, + windowTextHeight, + windowLineSpacing, + windowLineHeight, // = windowTextHeight + windowLineSpacing + windowMinimumHeight, + + usersOnline, // Raw users data + linesOfUsers = [], // Array of indexes pointing into usersOnline + numUsersToDisplay = 0, + firstUserToDisplay = 0, + + API_URL = "https://metaverse.highfidelity.com/api/v1/users?status=online", + API_FRIENDS_FILTER = "&filter=friends", + HTTP_GET_TIMEOUT = 60000, // ms = 1 minute + usersRequest, + processUsers, + pollUsersTimedOut, + usersTimer = null, + USERS_UPDATE_TIMEOUT = 5000, // ms = 5s + + showMe, + myVisibility, + + MENU_NAME = "View", + MENU_ITEM = "Users Online", + MENU_ITEM_OVERLAYS = "Overlays", + MENU_ITEM_AFTER = MENU_ITEM_OVERLAYS, + + SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", + SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", + SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", + SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + // +ve x, y values are offset from left, top of screen; -ve from right, bottom. + + isLoggedIn = false, + isVisible = true, + isMinimized = false, + isBorderVisible = false, + + viewport, + isMirrorDisplay = false, + isFullscreenMirror = false, + + windowPosition = {}, // Bottom left corner of window pane. + isMovingWindow = false, + movingClickOffset = { x: 0, y: 0 }, + + isUsingScrollbars = false, + isMovingScrollbar = false, + scrollbarBackgroundPosition = {}, + scrollbarBarPosition = {}, + scrollbarBarClickedAt, // 0.0 .. 1.0 + scrollbarValue = 0.0; // 0.0 .. 1.0 + + function isWindowDisabled() { + return !Menu.isOptionChecked(MENU_ITEM) || !Menu.isOptionChecked(MENU_ITEM_OVERLAYS); + } + + function isValueTrue(value) { + // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after + // Being written if refresh script. + return value === true || value === "true"; + } + + function calculateWindowHeight() { + var AUDIO_METER_HEIGHT = 52, + MIRROR_HEIGHT = 220, + nonUsersHeight, + maxWindowHeight; + + if (isMinimized) { + windowHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; + return; + } + + // Reserve space for title, friends button, and option controls + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + + (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) + + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; + + // Limit window to height of viewport above window position minus VU meter and mirror if displayed + windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; + maxWindowHeight = windowPosition.y - AUDIO_METER_HEIGHT; + if (isMirrorDisplay && !isFullscreenMirror) { + maxWindowHeight -= MIRROR_HEIGHT; + } + windowHeight = Math.max(Math.min(windowHeight, maxWindowHeight), nonUsersHeight); + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; + + // Corresponding number of users to actually display + numUsersToDisplay = Math.max(Math.round((windowHeight - nonUsersHeight) / windowLineHeight), 0); + isUsingScrollbars = 0 < numUsersToDisplay && numUsersToDisplay < linesOfUsers.length; + if (isUsingScrollbars) { + firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); + } else { + firstUserToDisplay = 0; + scrollbarValue = 0.0; + } + } + + function saturateWindowPosition() { + windowPosition.x = Math.max(WINDOW_MARGIN_X, Math.min(viewport.x - WINDOW_WIDTH - WINDOW_MARGIN_X, windowPosition.x)); + windowPosition.y = Math.max(windowMinimumHeight, Math.min(viewport.y, windowPosition.y)); + } + + function updateOverlayPositions() { + // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. + var windowLeft = windowPosition.x, + windowTop = windowPosition.y - windowHeight, + x, + y; + + Overlays.editOverlay(windowBorder, { + x: windowPosition.x - WINDOW_BORDER_LEFT_MARGIN, + y: windowTop - WINDOW_BORDER_TOP_MARGIN + }); + Overlays.editOverlay(windowPane, { + x: windowLeft, + y: windowTop + }); + Overlays.editOverlay(windowHeading, { + x: windowLeft + WINDOW_MARGIN, + y: windowTop + WINDOW_MARGIN + }); + + Overlays.editOverlay(minimizeButton, { + x: windowLeft + WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, + y: windowTop + WINDOW_MARGIN + }); + + scrollbarBackgroundPosition.x = windowLeft + WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH; + scrollbarBackgroundPosition.y = windowTop + WINDOW_MARGIN + windowTextHeight; + Overlays.editOverlay(scrollbarBackground, { + x: scrollbarBackgroundPosition.x, + y: scrollbarBackgroundPosition.y + }); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + Overlays.editOverlay(scrollbarBar, { + x: scrollbarBackgroundPosition.x + 1, + y: scrollbarBarPosition.y + }); + + + x = windowLeft + WINDOW_MARGIN; + y = windowPosition.y - + DISPLAY_SPACER - + windowLineHeight - VISIBILITY_SPACER - + windowLineHeight - WINDOW_BASE_MARGIN; + if (shouldShowFriendsButton()) { + y -= FRIENDS_BUTTON_HEIGHT; + Overlays.editOverlay(friendsButton, { + x: x, + y: y + }); + y += FRIENDS_BUTTON_HEIGHT; + } + + y += DISPLAY_SPACER; + displayControl.updatePosition(x, y); + + y += windowLineHeight + VISIBILITY_SPACER; + visibilityControl.updatePosition(x, y); + } + + function updateUsersDisplay() { + var displayText = "", + user, + userText, + textWidth, + maxTextWidth, + ellipsisWidth, + reducedTextWidth, + i; + + if (!isMinimized) { + maxTextWidth = WINDOW_WIDTH - (isUsingScrollbars ? SCROLLBAR_BACKGROUND_WIDTH : 0) - 2 * WINDOW_MARGIN; + ellipsisWidth = Overlays.textSize(windowPane, "...").width; + reducedTextWidth = maxTextWidth - ellipsisWidth; + + for (i = 0; i < numUsersToDisplay; i += 1) { + user = usersOnline[linesOfUsers[firstUserToDisplay + i]]; + userText = user.text; + textWidth = user.textWidth; + + if (textWidth > maxTextWidth) { + // Trim and append "..." to fit window width + maxTextWidth = maxTextWidth - Overlays.textSize(windowPane, "...").width; + while (textWidth > reducedTextWidth) { + userText = userText.slice(0, -1); + textWidth = Overlays.textSize(windowPane, userText).width; + } + userText += "..."; + } + + displayText += "\n" + userText; + } + + displayText = displayText.slice(1); // Remove leading "\n". + + scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; + Overlays.editOverlay(scrollbarBackground, { + height: scrollbarBackgroundHeight, + visible: isLoggedIn && isUsingScrollbars + }); + scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, + SCROLLBAR_BAR_MIN_HEIGHT); + Overlays.editOverlay(scrollbarBar, { + height: scrollbarBarHeight, + visible: isLoggedIn && isUsingScrollbars + }); + } + + Overlays.editOverlay(windowBorder, { + height: windowBorderHeight + }); + + Overlays.editOverlay(windowPane, { + height: windowHeight, + text: displayText + }); + + Overlays.editOverlay(windowHeading, { + text: isLoggedIn ? (linesOfUsers.length > 0 ? "Users online" : "No users online") : "Users online - log in to view" + }); + } + + function shouldShowFriendsButton() { + return isVisible && isLoggedIn && !isMinimized; + } + + function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isVisible && isBorderVisible + }); + Overlays.editOverlay(windowPane, { + visible: isVisible + }); + Overlays.editOverlay(windowHeading, { + visible: isVisible + }); + Overlays.editOverlay(minimizeButton, { + visible: isVisible + }); + Overlays.editOverlay(scrollbarBackground, { + visible: isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(scrollbarBar, { + visible: isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(friendsButton, { + visible: shouldShowFriendsButton() + }); + displayControl.setVisible(isVisible && !isMinimized); + visibilityControl.setVisible(isVisible && !isMinimized); + } + + function checkLoggedIn() { + var wasLoggedIn = isLoggedIn; + + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn !== wasLoggedIn) { + if (wasLoggedIn) { + setMinimized(true); + calculateWindowHeight(); + updateOverlayPositions(); + updateUsersDisplay(); + } + + updateOverlayVisibility(); + } + } + + function pollUsers() { + var url = API_URL; + + if (showMe === DISPLAY_FRIENDS) { + url += API_FRIENDS_FILTER; + } + + usersRequest = new XMLHttpRequest(); + usersRequest.open("GET", url, true); + usersRequest.timeout = HTTP_GET_TIMEOUT; + usersRequest.ontimeout = pollUsersTimedOut; + usersRequest.onreadystatechange = processUsers; + usersRequest.send(); + } + + processUsers = function () { + var response, + myUsername, + user, + userText, + i; + + if (usersRequest.readyState === usersRequest.DONE) { + if (usersRequest.status === 200) { + response = JSON.parse(usersRequest.responseText); + if (response.status !== "success") { + print("Error: Request for users status returned status = " + response.status); + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + return; + } + if (!response.hasOwnProperty("data") || !response.data.hasOwnProperty("users")) { + print("Error: Request for users status returned invalid data"); + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + return; + } + + usersOnline = response.data.users; + myUsername = GlobalServices.username; + linesOfUsers = []; + for (i = 0; i < usersOnline.length; i += 1) { + user = usersOnline[i]; + if (user.username !== myUsername && user.online) { + userText = user.username; + if (user.location.root) { + userText += " @ " + user.location.root.name; + } + + usersOnline[i].text = userText; + usersOnline[i].textWidth = Overlays.textSize(windowPane, userText).width; + + linesOfUsers.push(i); + } + } + + checkLoggedIn(); + calculateWindowHeight(); + updateUsersDisplay(); + updateOverlayPositions(); + + } else { + print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + return; + } + + usersTimer = Script.setTimeout(pollUsers, USERS_UPDATE_TIMEOUT); // Update after finished processing. + } + }; + + pollUsersTimedOut = function () { + print("Error: Request for users status timed out"); + usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. + }; + + function setVisible(visible) { + isVisible = visible; + + if (isVisible) { + if (usersTimer === null) { + pollUsers(); + } + } else { + Script.clearTimeout(usersTimer); + usersTimer = null; + } + + updateOverlayVisibility(); + } + + function setMinimized(minimized) { + isMinimized = minimized; + Overlays.editOverlay(minimizeButton, { + subImage: { + y: isMinimized ? MIN_MAX_BUTTON_SVG_HEIGHT / 2 : 0 + } + }); + updateOverlayVisibility(); + Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); + } + + function onMenuItemEvent(event) { + if (event === MENU_ITEM) { + setVisible(Menu.isOptionChecked(MENU_ITEM)); + } + } + + function onFindableByChanged(event) { + if (VISIBILITY_VALUES.indexOf(event) !== -1) { + myVisibility = event; + visibilityControl.setValue(event); + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); + } else { + print("Error: Unrecognized onFindableByChanged value: " + event); + } + } + + function onMousePressEvent(event) { + var clickedOverlay, + numLinesBefore, + overlayX, + overlayY, + minY, + maxY, + lineClicked, + userClicked, + delta; + + if (!isVisible || isWindowDisabled()) { + return; + } + + clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + + if (displayControl.handleClick(clickedOverlay)) { + if (usersTimer !== null) { + Script.clearTimeout(usersTimer); + usersTimer = null; + } + pollUsers(); + showMe = displayControl.getValue(); + Settings.setValue(SETTING_USERS_SHOW_ME, showMe); + return; + } + + if (visibilityControl.handleClick(clickedOverlay)) { + myVisibility = visibilityControl.getValue(); + GlobalServices.findableBy = myVisibility; + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); + return; + } + + if (clickedOverlay === windowPane) { + + overlayX = event.x - windowPosition.x - WINDOW_MARGIN; + overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; + + numLinesBefore = Math.round(overlayY / windowLineHeight); + minY = numLinesBefore * windowLineHeight; + maxY = minY + windowTextHeight; + + lineClicked = -1; + if (minY <= overlayY && overlayY <= maxY) { + lineClicked = numLinesBefore; + } + + userClicked = firstUserToDisplay + lineClicked; + + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && + overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); + location.goToUser(usersOnline[linesOfUsers[userClicked]].username); + } + + return; + } + + if (clickedOverlay === minimizeButton) { + setMinimized(!isMinimized); + calculateWindowHeight(); + updateOverlayPositions(); + updateUsersDisplay(); + return; + } + + if (clickedOverlay === scrollbarBar) { + scrollbarBarClickedAt = (event.y - scrollbarBarPosition.y) / scrollbarBarHeight; + Overlays.editOverlay(scrollbarBar, { + backgroundAlpha: SCROLLBAR_BAR_SELECTED_ALPHA + }); + isMovingScrollbar = true; + return; + } + + if (clickedOverlay === scrollbarBackground) { + delta = scrollbarBarHeight / (scrollbarBackgroundHeight - scrollbarBarHeight); + + if (event.y < scrollbarBarPosition.y) { + scrollbarValue = Math.max(scrollbarValue - delta, 0.0); + } else { + scrollbarValue = Math.min(scrollbarValue + delta, 1.0); + } + + firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); + updateOverlayPositions(); + updateUsersDisplay(); + return; + } + + if (clickedOverlay === friendsButton) { + if (!friendsWindow) { + friendsWindow = new OverlayWebWindow({ + title: FRIENDS_WINDOW_TITLE, + width: FRIENDS_WINDOW_WIDTH, + height: FRIENDS_WINDOW_HEIGHT, + visible: false + }); + } friendsWindow.setURL(FRIENDS_WINDOW_URL); friendsWindow.setVisible(true); friendsWindow.raise(); + return; + } + + if (clickedOverlay === windowBorder) { + movingClickOffset = { + x: event.x - windowPosition.x, + y: event.y - windowPosition.y + }; + + isMovingWindow = true; } } - button.clicked.connect(onClicked); - tablet.webEventReceived.connect(onWebEventReceived); + function onMouseMoveEvent(event) { + var isVisible; - function cleanup() { - button.clicked.disconnect(onClicked); - tablet.removeButton(button); + if (!isLoggedIn || isWindowDisabled()) { + return; + } + + if (isMovingScrollbar) { + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && + event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && + scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && + event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / + (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); + firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); + updateOverlayPositions(); + updateUsersDisplay(); + } else { + Overlays.editOverlay(scrollbarBar, { + backgroundAlpha: SCROLLBAR_BAR_ALPHA + }); + isMovingScrollbar = false; + } + } + + if (isMovingWindow) { + windowPosition = { + x: event.x - movingClickOffset.x, + y: event.y - movingClickOffset.y + }; + + saturateWindowPosition(); + calculateWindowHeight(); + updateOverlayPositions(); + updateUsersDisplay(); + + } else { + + isVisible = isBorderVisible; + if (isVisible) { + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && + event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && + windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && + event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + } else { + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && + windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + } + if (isVisible !== isBorderVisible) { + isBorderVisible = isVisible; + Overlays.editOverlay(windowBorder, { + visible: isBorderVisible + }); + } + } } - Script.scriptEnding.connect(cleanup); + function onMouseReleaseEvent() { + var offset = {}; + + if (isWindowDisabled()) { + return; + } + + if (isMovingScrollbar) { + Overlays.editOverlay(scrollbarBar, { + backgroundAlpha: SCROLLBAR_BAR_ALPHA + }); + isMovingScrollbar = false; + } + + if (isMovingWindow) { + // Save offset of bottom of window to nearest edge of the window. + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? + windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) ? + windowPosition.y : windowPosition.y - viewport.y; + Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + isMovingWindow = false; + } + } + + function onScriptUpdate() { + var oldViewport = viewport, + oldIsMirrorDisplay = isMirrorDisplay, + oldIsFullscreenMirror = isFullscreenMirror, + MIRROR_MENU_ITEM = "Mirror", + FULLSCREEN_MIRROR_MENU_ITEM = "Fullscreen Mirror"; + + if (isWindowDisabled()) { + return; + } + + viewport = Controller.getViewportDimensions(); + isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); + isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); + + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || + isFullscreenMirror !== oldIsFullscreenMirror) { + calculateWindowHeight(); + updateUsersDisplay(); + } + + if (viewport.y !== oldViewport.y) { + if (windowPosition.y > oldViewport.y / 2) { + // Maintain position w.r.t. bottom of window. + windowPosition.y = viewport.y - (oldViewport.y - windowPosition.y); + } + } + + if (viewport.x !== oldViewport.x) { + if (windowPosition.x + (WINDOW_WIDTH / 2) > oldViewport.x / 2) { + // Maintain position w.r.t. right of window. + windowPosition.x = viewport.x - (oldViewport.x - windowPosition.x); + } + } + + updateOverlayPositions(); + } + + function setUp() { + var textSizeOverlay, + offsetSetting, + offset = {}, + hmdViewport; + + textSizeOverlay = Overlays.addOverlay("text", { + font: WINDOW_FONT, + visible: false + }); + windowTextHeight = Math.floor(Overlays.textSize(textSizeOverlay, "1").height); + windowLineSpacing = Math.floor(Overlays.textSize(textSizeOverlay, "1\n2").height - 2 * windowTextHeight); + windowLineHeight = windowTextHeight + windowLineSpacing; + windowMinimumHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; + Overlays.deleteOverlay(textSizeOverlay); + + viewport = Controller.getViewportDimensions(); + + offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); + if (offsetSetting !== "") { + offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); + } + if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { + windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; + windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; + } else { + hmdViewport = Controller.getRecommendedOverlayRect(); + windowPosition = { + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + }; + } + + saturateWindowPosition(); + calculateWindowHeight(); + + windowBorder = Overlays.addOverlay("rectangle", { + x: 0, + y: viewport.y, // Start up off-screen + width: WINDOW_BORDER_WIDTH, + height: windowBorderHeight, + radius: WINDOW_BORDER_RADIUS, + color: WINDOW_BORDER_COLOR, + alpha: WINDOW_BORDER_ALPHA, + visible: false + }); + + windowPane = Overlays.addOverlay("text", { + x: 0, + y: viewport.y, + width: WINDOW_WIDTH, + height: windowHeight, + topMargin: WINDOW_MARGIN + windowLineHeight, + leftMargin: WINDOW_MARGIN, + color: WINDOW_FOREGROUND_COLOR, + alpha: WINDOW_FOREGROUND_ALPHA, + backgroundColor: WINDOW_BACKGROUND_COLOR, + backgroundAlpha: WINDOW_BACKGROUND_ALPHA, + text: "", + font: WINDOW_FONT, + visible: false + }); + + windowHeading = Overlays.addOverlay("text", { + x: 0, + y: viewport.y, + width: WINDOW_WIDTH - 2 * WINDOW_MARGIN, + height: windowTextHeight, + topMargin: 0, + leftMargin: 0, + color: WINDOW_HEADING_COLOR, + alpha: WINDOW_HEADING_ALPHA, + backgroundAlpha: 0.0, + text: "Users online", + font: WINDOW_FONT, + visible: false + }); + + minimizeButton = Overlays.addOverlay("image", { + x: 0, + y: viewport.y, + width: MIN_MAX_BUTTON_WIDTH, + height: MIN_MAX_BUTTON_HEIGHT, + imageURL: MIN_MAX_BUTTON_SVG, + subImage: { + x: 0, + y: 0, + width: MIN_MAX_BUTTON_SVG_WIDTH, + height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 + }, + color: MIN_MAX_BUTTON_COLOR, + alpha: MIN_MAX_BUTTON_ALPHA, + visible: false + }); + + scrollbarBackgroundPosition = { + x: 0, + y: viewport.y + }; + scrollbarBackground = Overlays.addOverlay("text", { + x: 0, + y: scrollbarBackgroundPosition.y, + width: SCROLLBAR_BACKGROUND_WIDTH, + height: windowTextHeight, + backgroundColor: SCROLLBAR_BACKGROUND_COLOR, + backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, + text: "", + visible: false + }); + + scrollbarBarPosition = { + x: 0, + y: viewport.y + }; + scrollbarBar = Overlays.addOverlay("text", { + x: 0, + y: scrollbarBarPosition.y, + width: SCROLLBAR_BACKGROUND_WIDTH - 2, + height: windowTextHeight, + backgroundColor: SCROLLBAR_BAR_COLOR, + backgroundAlpha: SCROLLBAR_BAR_ALPHA, + text: "", + visible: false + }); + + friendsButton = Overlays.addOverlay("image", { + x: 0, + y: viewport.y, + width: FRIENDS_BUTTON_WIDTH, + height: FRIENDS_BUTTON_HEIGHT, + imageURL: FRIENDS_BUTTON_SVG, + subImage: { + x: 0, + y: 0, + width: FRIENDS_BUTTON_SVG_WIDTH, + height: FRIENDS_BUTTON_SVG_HEIGHT + }, + color: FRIENDS_BUTTON_COLOR, + alpha: FRIENDS_BUTTON_ALPHA, + visible: false + }); + + showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); + if (DISPLAY_VALUES.indexOf(showMe) === -1) { + showMe = DISPLAY_EVERYONE; + } + + displayControl = new PopUpMenu({ + prompt: DISPLAY_PROMPT, + value: showMe, + values: DISPLAY_VALUES, + displayValues: DISPLAY_DISPLAY_VALUES, + x: 0, + y: viewport.y, + width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, + promptWidth: DISPLAY_PROMPT_WIDTH, + lineHeight: windowLineHeight, + textHeight: windowTextHeight, + font: WINDOW_FONT, + promptColor: WINDOW_HEADING_COLOR, + promptAlpha: WINDOW_HEADING_ALPHA, + promptBackgroundColor: WINDOW_BACKGROUND_COLOR, + promptBackgroundAlpha: 0.0, + optionColor: WINDOW_FOREGROUND_COLOR, + optionAlpha: WINDOW_FOREGROUND_ALPHA, + optionBackgroundColor: OPTION_BACKGROUND_COLOR, + optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, + popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, + popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, + buttonColor: MIN_MAX_BUTTON_COLOR, + buttonAlpha: MIN_MAX_BUTTON_ALPHA, + visible: false + }); + + myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); + if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { + myVisibility = VISIBILITY_FRIENDS; + } + GlobalServices.findableBy = myVisibility; + + visibilityControl = new PopUpMenu({ + prompt: VISIBILITY_PROMPT, + value: myVisibility, + values: VISIBILITY_VALUES, + displayValues: VISIBILITY_DISPLAY_VALUES, + x: 0, + y: viewport.y, + width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, + promptWidth: VISIBILITY_PROMPT_WIDTH, + lineHeight: windowLineHeight, + textHeight: windowTextHeight, + font: WINDOW_FONT, + promptColor: WINDOW_HEADING_COLOR, + promptAlpha: WINDOW_HEADING_ALPHA, + promptBackgroundColor: WINDOW_BACKGROUND_COLOR, + promptBackgroundAlpha: 0.0, + optionColor: WINDOW_FOREGROUND_COLOR, + optionAlpha: WINDOW_FOREGROUND_ALPHA, + optionBackgroundColor: OPTION_BACKGROUND_COLOR, + optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, + popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, + popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, + buttonColor: MIN_MAX_BUTTON_COLOR, + buttonAlpha: MIN_MAX_BUTTON_ALPHA, + visible: false + }); + + Controller.mousePressEvent.connect(onMousePressEvent); + Controller.mouseMoveEvent.connect(onMouseMoveEvent); + Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); + + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: MENU_ITEM, + afterItem: MENU_ITEM_AFTER, + isCheckable: true, + isChecked: isVisible + }); + Menu.menuItemEvent.connect(onMenuItemEvent); + + GlobalServices.findableByChanged.connect(onFindableByChanged); + + Script.update.connect(onScriptUpdate); + + pollUsers(); + + // Set minimized at end - setup code does not handle `minimized == false` correctly + setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, true))); + } + + function tearDown() { + Menu.removeMenuItem(MENU_NAME, MENU_ITEM); + + Script.clearTimeout(usersTimer); + Overlays.deleteOverlay(windowBorder); + Overlays.deleteOverlay(windowPane); + Overlays.deleteOverlay(windowHeading); + Overlays.deleteOverlay(minimizeButton); + Overlays.deleteOverlay(scrollbarBackground); + Overlays.deleteOverlay(scrollbarBar); + Overlays.deleteOverlay(friendsButton); + displayControl.tearDown(); + visibilityControl.tearDown(); + } + + setUp(); + Script.scriptEnding.connect(tearDown); +}()); + +function cleanup () { + //remove tablet button + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } +} +Script.scriptEnding.connect(cleanup); + }()); // END LOCAL_SCOPE