From 07a542e35db68a04736ed06522ed95a2963d6c92 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Tue, 23 Jan 2018 11:43:29 -0300 Subject: [PATCH 1/8] Add bottom bar and address dialog (Android) --- android/app/build.gradle | 2 +- .../resources/icons/+android/backward.svg | 70 ++ .../resources/icons/+android/forward.svg | 71 ++ interface/resources/icons/+android/go-a.svg | 40 + interface/resources/icons/+android/go-i.svg | 40 + interface/resources/icons/+android/goto-a.svg | 28 + interface/resources/icons/+android/goto-i.svg | 28 + interface/resources/icons/+android/hide.svg | 947 ++++++++++++++++++ interface/resources/icons/+android/home.svg | 82 ++ .../resources/icons/+android/show-up.svg | 8 + .../qml/+android/AddressBarDialog.qml | 230 +++++ .../qml/hifi/+android/HifiConstants.qml | 54 + .../qml/hifi/+android/WindowHeader.qml | 113 +++ .../qml/hifi/+android/bottomHudOptions.qml | 89 ++ .../resources/qml/hifi/+android/bottombar.qml | 135 +++ .../resources/qml/hifi/+android/button.qml | 230 +++++ interface/src/Application.cpp | 2 + libraries/ui/src/QmlFragmentClass.cpp | 88 ++ libraries/ui/src/QmlFragmentClass.h | 45 + libraries/ui/src/ui/OffscreenQmlSurface.cpp | 13 +- scripts/+android/defaultScripts.js | 3 +- scripts/system/bottombar.js | 159 +++ scripts/system/goto-android.js | 96 ++ 23 files changed, 2570 insertions(+), 3 deletions(-) create mode 100644 interface/resources/icons/+android/backward.svg create mode 100644 interface/resources/icons/+android/forward.svg create mode 100755 interface/resources/icons/+android/go-a.svg create mode 100644 interface/resources/icons/+android/go-i.svg create mode 100755 interface/resources/icons/+android/goto-a.svg create mode 100644 interface/resources/icons/+android/goto-i.svg create mode 100644 interface/resources/icons/+android/hide.svg create mode 100644 interface/resources/icons/+android/home.svg create mode 100644 interface/resources/icons/+android/show-up.svg create mode 100644 interface/resources/qml/+android/AddressBarDialog.qml create mode 100644 interface/resources/qml/hifi/+android/HifiConstants.qml create mode 100644 interface/resources/qml/hifi/+android/WindowHeader.qml create mode 100644 interface/resources/qml/hifi/+android/bottomHudOptions.qml create mode 100644 interface/resources/qml/hifi/+android/bottombar.qml create mode 100644 interface/resources/qml/hifi/+android/button.qml create mode 100644 libraries/ui/src/QmlFragmentClass.cpp create mode 100644 libraries/ui/src/QmlFragmentClass.h create mode 100644 scripts/system/bottombar.js create mode 100644 scripts/system/goto-android.js diff --git a/android/app/build.gradle b/android/app/build.gradle index 1cff708c3b..3f8f43110a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,7 +23,7 @@ android { '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, '-DBUILD_BRANCH=' + BUILD_BRANCH, - '-DDISABLE_QML=ON', + '-DDISABLE_QML=OFF', '-DDISABLE_KTX_CACHE=ON' } } diff --git a/interface/resources/icons/+android/backward.svg b/interface/resources/icons/+android/backward.svg new file mode 100644 index 0000000000..ae0893fc66 --- /dev/null +++ b/interface/resources/icons/+android/backward.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/+android/forward.svg b/interface/resources/icons/+android/forward.svg new file mode 100644 index 0000000000..d03c4097d7 --- /dev/null +++ b/interface/resources/icons/+android/forward.svg @@ -0,0 +1,71 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/+android/go-a.svg b/interface/resources/icons/+android/go-a.svg new file mode 100755 index 0000000000..faecb15292 --- /dev/null +++ b/interface/resources/icons/+android/go-a.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/go-i.svg b/interface/resources/icons/+android/go-i.svg new file mode 100644 index 0000000000..0f1298d573 --- /dev/null +++ b/interface/resources/icons/+android/go-i.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/goto-a.svg b/interface/resources/icons/+android/goto-a.svg new file mode 100755 index 0000000000..5fb3e52e4c --- /dev/null +++ b/interface/resources/icons/+android/goto-a.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/goto-i.svg b/interface/resources/icons/+android/goto-i.svg new file mode 100644 index 0000000000..7613beb9e7 --- /dev/null +++ b/interface/resources/icons/+android/goto-i.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/hide.svg b/interface/resources/icons/+android/hide.svg new file mode 100644 index 0000000000..e09d517b08 --- /dev/null +++ b/interface/resources/icons/+android/hide.svg @@ -0,0 +1,947 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/home.svg b/interface/resources/icons/+android/home.svg new file mode 100644 index 0000000000..414c179e79 --- /dev/null +++ b/interface/resources/icons/+android/home.svg @@ -0,0 +1,82 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/+android/show-up.svg b/interface/resources/icons/+android/show-up.svg new file mode 100644 index 0000000000..0736b9a794 --- /dev/null +++ b/interface/resources/icons/+android/show-up.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/interface/resources/qml/+android/AddressBarDialog.qml b/interface/resources/qml/+android/AddressBarDialog.qml new file mode 100644 index 0000000000..e07af24b8d --- /dev/null +++ b/interface/resources/qml/+android/AddressBarDialog.qml @@ -0,0 +1,230 @@ +// +// AddressBarDialog.qml +// +// Created by Austin Davis on 2015/04/14 +// 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 +// + +import Hifi 1.0 +import QtQuick 2.4 +import "../controls" +import "../styles" +import "../hifi" as QmlHifi +import "../hifi/toolbars" +import "../styles-uit" as HifiStyles +import "../controls-uit" as HifiControls + +Item { + QmlHifi.HifiConstants { id: android } + + width: parent ? parent.width - android.dimen.windowLessWidth : 0 + height: parent ? parent.height - android.dimen.windowLessHeight : 0 + z: android.dimen.windowZ + anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom } + + id: bar + property bool isCursorVisible: false // Override default cursor visibility. + property bool shown: true + + onShownChanged: { + bar.visible = shown; + sendToScript({method: 'shownChanged', params: { shown: shown }}); + if (shown) { + updateLocationText(false); + } + } + + function hide() { + shown = false; + sendToScript ({ type: "hide" }); + } + + Component.onCompleted: { + updateLocationText(false); + } + + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifiStyleConstants } + + signal sendToScript(var message); + + AddressBarDialog { + id: addressBarDialog + } + + + Rectangle { + id: background + gradient: Gradient { + GradientStop { position: 0.0; color: android.color.gradientTop } + GradientStop { position: 1.0; color: android.color.gradientBottom } + } + anchors { + fill: parent + } + + QmlHifi.WindowHeader { + id: header + iconSource: "../../../icons/goto-i.svg" + titleText: "GO TO" + } + + HifiStyles.RalewayRegular { + id: notice + text: "YOUR LOCATION" + font.pixelSize: hifi.fonts.pixelSize * 2.15; + color: "#2CD7FF" + anchors { + bottom: addressBackground.top + bottomMargin: 45 + left: addressBackground.left + leftMargin: 60 + } + + } + + property int inputAreaHeight: 210 + property int inputAreaStep: (height - inputAreaHeight) / 2 + + ToolbarButton { + id: homeButton + y: 280 + imageURL: "../../icons/home.svg" + onClicked: { + addressBarDialog.loadHome(); + bar.shown = false; + } + anchors { + leftMargin: 75 + left: parent.left + } + size: 150 + } + + ToolbarButton { + id: backArrow; + imageURL: "../../icons/backward.svg"; + onClicked: addressBarDialog.loadBack(); + anchors { + left: homeButton.right + leftMargin: 70 + verticalCenter: homeButton.verticalCenter + } + size: 150 + } + ToolbarButton { + id: forwardArrow; + imageURL: "../../icons/forward.svg"; + onClicked: addressBarDialog.loadForward(); + anchors { + left: backArrow.right + leftMargin: 60 + verticalCenter: homeButton.verticalCenter + } + size: 150 + } + + HifiStyles.FiraSansRegular { + id: location; + font.pixelSize: addressLine.font.pixelSize; + color: "gray"; + clip: true; + anchors.fill: addressLine; + visible: addressLine.text.length === 0 + z: 1 + } + + Rectangle { + id: addressBackground + x: 780 + y: 280 + width: 1440 + height: 150 + color: "#FFFFFF" + } + + TextInput { + id: addressLine + focus: true + x: 870 + y: 450 + width: 1350 + height: 120 + inputMethodHints: Qt.ImhNoPredictiveText + //helperText: "Hint is here" + anchors { + verticalCenter: homeButton.verticalCenter + } + font.pixelSize: hifi.fonts.pixelSize * 3.75 + onTextChanged: { + //filterChoicesByText(); + updateLocationText(addressLine.text.length > 0); + if (!isCursorVisible && text.length > 0) { + isCursorVisible = true; + cursorVisible = true; + } + } + + onActiveFocusChanged: { + //cursorVisible = isCursorVisible && focus; + } + } + + + + function toggleOrGo() { + if (addressLine.text !== "") { + addressBarDialog.loadAddress(addressLine.text); + } + bar.shown = false; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + clearAddressLineTimer.start(); + event.accepted = true + bar.shown = false; + break + case Qt.Key_Enter: + case Qt.Key_Return: + toggleOrGo(); + clearAddressLineTimer.start(); + event.accepted = true + break + } + } + + } + + Timer { + // Delay clearing address line so as to avoid flicker of "not connected" being displayed after entering an address. + id: clearAddressLineTimer + running: false + interval: 100 // ms + repeat: false + onTriggered: { + addressLine.text = ""; + isCursorVisible = false; + } + } + + function updateLocationText(enteringAddress) { + if (enteringAddress) { + notice.text = "Go to a place, @user, path or network address"; + notice.color = "#ffffff"; // hifiStyleConstants.colors.baseGrayHighlight; + location.visible = false; + } else { + notice.text = AddressManager.isConnected ? "YOUR LOCATION:" : "NOT CONNECTED"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.turquoise : hifiStyleConstants.colors.redHighlight; + // Display hostname, which includes ip address, localhost, and other non-placenames. + location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); + location.visible = true; + } + } + +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/+android/HifiConstants.qml b/interface/resources/qml/hifi/+android/HifiConstants.qml new file mode 100644 index 0000000000..ee6d92ed38 --- /dev/null +++ b/interface/resources/qml/hifi/+android/HifiConstants.qml @@ -0,0 +1,54 @@ +// +// HifiAndroidConstants.qml +// interface/resources/qml/+android +// +// Created by Gabriel Calero & Cristian Duarte on 23 Oct 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 +// + +import QtQuick 2.4 + +Item { + + id: android + + readonly property alias dimen: dimen + readonly property alias color: color + + Item { + id: dimen + readonly property real windowLessWidth: 126 + readonly property real windowLessHeight: 64 + + readonly property real windowZ: 100 + + readonly property real headerHeight: 276 + + readonly property real headerIconPosX: 90 + readonly property real headerIconPosY: 108 + readonly property real headerIconWidth: 111 + readonly property real headerIconHeight: 111 + readonly property real headerIconTitleDistance: 151 + + readonly property real headerHideWidth: 150 + readonly property real headerHideHeight: 150 + readonly property real headerHideRightMargin: 110 + readonly property real headerHideTopMargin: 90 + readonly property real headerHideIconWidth: 70 + readonly property real headerHideIconHeight: 45 + readonly property real headerHideTextTopMargin: 36 + + readonly property real botomHudWidth: 366 + readonly property real botomHudHeight: 180 + + } + + Item { + id: color + readonly property color gradientTop: "#4E4E4E" + readonly property color gradientBottom: "#242424" + } +} diff --git a/interface/resources/qml/hifi/+android/WindowHeader.qml b/interface/resources/qml/hifi/+android/WindowHeader.qml new file mode 100644 index 0000000000..4ec0a0c6e6 --- /dev/null +++ b/interface/resources/qml/hifi/+android/WindowHeader.qml @@ -0,0 +1,113 @@ +// +// WindowHeader.qml +// interface/resources/qml/android +// +// Created by Gabriel Calero & Cristian Duarte on 23 Oct 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 +import Qt.labs.settings 1.0 +import "." +import "../styles" as HifiStyles +import "../styles-uit" +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls +import ".." + + +// header +Rectangle { + id: header + + // properties + property string iconSource: "" + property string titleText: "" + property var extraItemInCenter: Item {} + + HifiStyles.HifiConstants { id: hifiStylesConstants } + + /*property var mockRectangle: Rectangle { + anchors.fill: parent + color: "#44FFFF00" + }*/ + color: "#00000000" + //color: "#55FF0000" + width: parent.width + height: android.dimen.headerHeight + anchors.top : parent.top + + Image { + id: windowIcon + source: iconSource + x: android.dimen.headerIconPosX + y: android.dimen.headerIconPosY + width: android.dimen.headerIconWidth + height: android.dimen.headerIconHeight + } + + /*HifiStylesUit.*/FiraSansSemiBold { + id: windowTitle + x: windowIcon.x + android.dimen.headerIconTitleDistance + anchors.verticalCenter: windowIcon.verticalCenter + text: titleText + color: "#FFFFFF" + font.letterSpacing: 2 + font.pixelSize: hifiStylesConstants.fonts.headerPixelSize * 2.15 + } + Item { + height: 60 + anchors { + left: windowTitle.right + right: hideButton.left + verticalCenter: windowIcon.verticalCenter + } + children: [ extraItemInCenter/*, mockRectangle */] + } + + Rectangle { + id: hideButton + height: android.dimen.headerHideWidth + width: android.dimen.headerHideHeight + color: "#00000000" + //color: "#CC00FF00" + anchors { + top: parent.top + right: parent.right + rightMargin: android.dimen.headerHideRightMargin + topMargin: android.dimen.headerHideTopMargin + } + Image { + id: hideIcon + source: "../../../icons/hide.svg" + width: android.dimen.headerHideIconWidth + height: android.dimen.headerHideIconHeight + anchors { + horizontalCenter: parent.horizontalCenter + } + } + /*HifiStyles.*/FiraSansRegular { + anchors { + top: hideIcon.bottom + horizontalCenter: hideIcon.horizontalCenter + topMargin: android.dimen.headerHideTextTopMargin + } + text: "HIDE" + color: "#FFFFFF" + font.pixelSize: hifiStylesConstants.fonts.pixelSize * 2.15 + } + + MouseArea { + anchors.fill: parent + onClicked: { + hide(); + } + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/+android/bottomHudOptions.qml b/interface/resources/qml/hifi/+android/bottomHudOptions.qml new file mode 100644 index 0000000000..860298149f --- /dev/null +++ b/interface/resources/qml/hifi/+android/bottomHudOptions.qml @@ -0,0 +1,89 @@ +// +// bottomHudOptions.qml +// interface/resources/qml/android +// +// Created by Cristian Duarte & Gabriel Calero on 24 Nov 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 +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 +import Qt.labs.settings 1.0 +import "../../styles" as HifiStyles +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls +import ".." +import "." + +Item { + id: bottomHud + + property bool shown: false + + signal sendToScript(var message); + + HifiConstants { id: android } + + onShownChanged: { + bottomHud.visible = shown; + } + + function hide() { + shown = false; + } + + Rectangle { + anchors.fill : parent + color: "transparent" + Flow { + id: flowMain + spacing: 0 + flow: Flow.LeftToRight + layoutDirection: Flow.LeftToRight + anchors.fill: parent + anchors.margins: 12 + + Rectangle { + id: hideButton + height: android.dimen.headerHideWidth + width: android.dimen.headerHideHeight + color: "#00000000" + anchors { + horizontalCenter: parent.horizontalCenter + } + Image { + id: hideIcon + source: "../../../icons/show-up.svg" + width: android.dimen.headerHideIconWidth + height: android.dimen.headerHideIconHeight + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript ({ method: "showUpBar" }); + } + } + } + } + } + + Component.onCompleted: { + width = android.dimen.botomHudWidth; + height = android.dimen.botomHudHeight; + x=Window.innerWidth - width; + y=Window.innerHeight - height; + } + +} diff --git a/interface/resources/qml/hifi/+android/bottombar.qml b/interface/resources/qml/hifi/+android/bottombar.qml new file mode 100644 index 0000000000..2a34b7fe19 --- /dev/null +++ b/interface/resources/qml/hifi/+android/bottombar.qml @@ -0,0 +1,135 @@ +// +// bottomHudOptions.qml +// interface/resources/qml/android +// +// Created by Gabriel Calero & Cristian Duarte on 19 Jan 2018 +// 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 +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 +import Qt.labs.settings 1.0 +import "../../styles" as Styles +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls +import ".." +import "." + +Item { + id: bar + x:0 + + property bool shown: true + + signal sendToScript(var message); + + onShownChanged: { + bar.visible = shown; + } + + function hide() { + //shown = false; + sendToScript({ method: "hide" }); + } + + Styles.HifiConstants { id: hifi } + HifiConstants { id: android } + + Rectangle { + id: background + anchors.fill : parent + color: "#FF000000" + border.color: "#FFFFFF" + anchors.bottomMargin: -1 + anchors.leftMargin: -1 + anchors.rightMargin: -1 + Flow { + id: flowMain + spacing: 10 + anchors.fill: parent + anchors.topMargin: 12 + anchors.bottomMargin: 12 + anchors.rightMargin: 12 + anchors.leftMargin: 72 + } + + + Rectangle { + id: hideButton + height: android.dimen.headerHideWidth + width: android.dimen.headerHideHeight + color: "#00000000" + anchors { + right: parent.right + rightMargin: android.dimen.headerHideRightMargin + top: parent.top + topMargin: android.dimen.headerHideTopMargin + } + + Image { + id: hideIcon + source: "../../../icons/hide.svg" + width: android.dimen.headerHideIconWidth + height: android.dimen.headerHideIconHeight + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + } + } + FiraSansRegular { + anchors { + top: hideIcon.bottom + horizontalCenter: hideIcon.horizontalCenter + topMargin: 12 + } + text: "HIDE" + color: "#FFFFFF" + font.pixelSize: hifi.fonts.pixelSize * 2.5; + } + + MouseArea { + anchors.fill: parent + onClicked: { + hide(); + } + } + } + } + + Component.onCompleted: { + // put on bottom + width = Window.innerWidth; + height = 255; + y = Window.innerHeight - height; + } + + function addButton(properties) { + var component = Qt.createComponent("button.qml"); + if (component.status == Component.Ready) { + var button = component.createObject(flowMain); + // copy all properites to button + var keys = Object.keys(properties).forEach(function (key) { + button[key] = properties[key]; + }); + return button; + } else if( component.status == Component.Error) { + console.log("Load button errors " + component.errorString()); + } + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + +} diff --git a/interface/resources/qml/hifi/+android/button.qml b/interface/resources/qml/hifi/+android/button.qml new file mode 100644 index 0000000000..ec7af2ab92 --- /dev/null +++ b/interface/resources/qml/hifi/+android/button.qml @@ -0,0 +1,230 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 + +Item { + id: button + property string icon: "icons/edit-icon.svg" + property string hoverIcon: button.icon + property string activeIcon: button.icon + property string activeHoverIcon: button.activeIcon + property int stableOrder: 0 + + property int iconSize: 165 + property string text: "." + property string hoverText: button.text + property string activeText: button.text + property string activeHoverText: button.activeText + + property string bgColor: "#ffffff" + property string hoverBgColor: button.bgColor + property string activeBgColor: button.bgColor + property string activeHoverBgColor: button.bgColor + + property real bgOpacity: 0 + property real hoverBgOpacity: 1 + property real activeBgOpacity: 0.5 + property real activeHoverBgOpacity: 1 + + property string textColor: "#ffffff" + property int textSize: 54 + property string hoverTextColor: "#ffffff" + property string activeTextColor: "#ffffff" + property string activeHoverTextColor: "#ffffff" + property int bottomMargin: 30 + + property bool isEntered: false + property double sortOrder: 100 + + property bool isActive: false + + signal clicked() + + onIsActiveChanged: { + if (button.isEntered) { + button.state = (button.isActive) ? "hover active state" : "hover state"; + } else { + button.state = (button.isActive) ? "active state" : "base state"; + } + } + + function editProperties(props) { + for (var prop in props) { + button[prop] = props[prop]; + } + } + + + width: 300 + height: 300 + + Rectangle { + id: buttonBg + color: bgColor + opacity: bgOpacity + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + Image { + id: icon + width: iconSize + height: iconSize + anchors.bottom: text.top + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.Stretch + source: urlHelper(button.icon) + } + FontLoader { + id: firaSans + source: "../../../fonts/FiraSans-Regular.ttf" + } + Text { + id: text + color: "#ffffff" + text: button.text + font.family: "FiraSans" + //font.bold: true + font.pixelSize: textSize + anchors.bottom: parent.bottom + anchors.bottomMargin: bottomMargin + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + console.log("Bottom bar button clicked!!"); + /*if (tabletButton.inDebugMode) { + if (tabletButton.isActive) { + tabletButton.isActive = false; + } else { + tabletButton.isActive = true; + } + }*/ + button.clicked(); + /*if (tabletRoot) { + tabletRoot.playButtonClickSound(); + }*/ + } + onEntered: { + button.isEntered = true; + if (button.isActive) { + button.state = "hover active state"; + } else { + button.state = "hover state"; + } + } + onExited: { + button.isEntered = false; + if (button.isActive) { + button.state = "active state"; + } else { + button.state = "base state"; + } + } + } + states: [ + State { + name: "hover state" + + PropertyChanges { + target: buttonBg + //color: "#cfcfcf" + //opacity: 1 + color: button.hoverBgColor + opacity: button.hoverBgOpacity + } + + PropertyChanges { + target: text + //color: "#ffffff" + color: button.hoverTextColor + text: button.hoverText + } + + PropertyChanges { + target: icon + source: urlHelper(button.hoverIcon) + } + }, + State { + name: "active state" + + PropertyChanges { + target: buttonBg + //color: "#1fc6a6" + //opacity: 1 + color: button.activeBgColor + opacity: button.activeBgOpacity + } + + PropertyChanges { + target: text + //color: "#333333" + color: button.activeTextColor + text: button.activeText + } + + PropertyChanges { + target: icon + source: urlHelper(button.activeIcon) + } + }, + State { + name: "hover active state" + + PropertyChanges { + target: buttonBg + //color: "#ff0000" + //opacity: 1 + color: button.activeHoverBgColor + opacity: button.activeHoverBgOpacity + } + + PropertyChanges { + target: text + //color: "#333333" + color: button.activeHoverTextColor + text: button.activeHoverText + } + + PropertyChanges { + target: icon + source: urlHelper(button.activeHoverIcon) + } + }, + State { + name: "base state" + + PropertyChanges { + target: buttonBg + //color: "#9A9A9A" + //opacity: 0.1 + color: button.bgColor + opacity: button.bgOpacity + } + + PropertyChanges { + target: text + //color: "#ffffff" + color: button.textColor + text: button.text + } + + PropertyChanges { + target: icon + source: urlHelper(button.icon) + } + } + ] +} \ No newline at end of file diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8c427c0e39..67c3de743e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -138,6 +138,7 @@ #include #include #include +#include #include #include #include @@ -5861,6 +5862,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); #endif scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); + scriptEngine->registerFunction("QmlFragment", QmlFragmentClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); diff --git a/libraries/ui/src/QmlFragmentClass.cpp b/libraries/ui/src/QmlFragmentClass.cpp new file mode 100644 index 0000000000..ff67022305 --- /dev/null +++ b/libraries/ui/src/QmlFragmentClass.cpp @@ -0,0 +1,88 @@ +// +// Created by Gabriel Calero & Cristian Duarte on Aug 25, 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 +// + +#include "QmlFragmentClass.h" + +#include +#include +#include + +#include + +#include "OffscreenUi.h" + + +std::mutex QmlFragmentClass::_mutex; +std::map QmlFragmentClass::_fragments; + +QmlFragmentClass::QmlFragmentClass(QString id) : + qml(id) { +} +// Method called by Qt scripts to create a new bottom menu bar in Android +QScriptValue QmlFragmentClass::constructor(QScriptContext* context, QScriptEngine* engine) { + + std::lock_guard guard(_mutex); + auto qml = context->argument(0).toVariant().toMap().value("qml"); + if (qml.isValid()) { + // look up tabletId in the map. + auto iter = _fragments.find(qml.toString()); + if (iter != _fragments.end()) { + //qDebug() << "[QML-ANDROID] QmlFragmentClass menu already exists"; + return iter->second; + } + } else { + qWarning() << "QmlFragmentClass could not build instance " << qml; + return QScriptValue(); + } + + auto properties = parseArguments(context); + auto offscreenUi = DependencyManager::get(); + QmlFragmentClass* retVal = new QmlFragmentClass(qml.toString()); + Q_ASSERT(retVal); + if (QThread::currentThread() != qApp->thread()) { + retVal->moveToThread(qApp->thread()); + BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_ARG(QVariantMap, properties)); + } else { + retVal->initQml(properties); + } + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); + QScriptValue scriptObject = engine->newQObject(retVal); + _fragments[qml.toString()] = scriptObject; + return scriptObject; +} + +void QmlFragmentClass::close() { + QmlWindowClass::close(); + _fragments.erase(qml); +} + +QObject* QmlFragmentClass::addButton(const QVariant& properties) { + QVariant resultVar; + Qt::ConnectionType connectionType = Qt::AutoConnection; + + if (QThread::currentThread() != _qmlWindow->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + bool hasResult = QMetaObject::invokeMethod(_qmlWindow, "addButton", connectionType, + Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); + if (!hasResult) { + qWarning() << "QmlFragmentClass addButton has no result"; + return NULL; + } + + QObject* qmlButton = qvariant_cast(resultVar); + if (!qmlButton) { + qWarning() << "QmlFragmentClass addButton result not a QObject"; + return NULL; + } + + return qmlButton; +} + +void QmlFragmentClass::removeButton(QObject* button) { +} diff --git a/libraries/ui/src/QmlFragmentClass.h b/libraries/ui/src/QmlFragmentClass.h new file mode 100644 index 0000000000..87c18a49ad --- /dev/null +++ b/libraries/ui/src/QmlFragmentClass.h @@ -0,0 +1,45 @@ +// +// Created by Gabriel Calero & Cristian Duarte on Aug 25, 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 +// + +#ifndef hifi_ui_QmlFragmentClass_h +#define hifi_ui_QmlFragmentClass_h + +#include "QmlWindowClass.h" + +class QmlFragmentClass : public QmlWindowClass { + Q_OBJECT +public: + QmlFragmentClass(QString id); + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + + /**jsdoc + * Creates a new button, adds it to this and returns it. + * @function QmlFragmentClass#addButton + * @param properties {Object} button properties + * @returns {TabletButtonProxy} + */ + Q_INVOKABLE QObject* addButton(const QVariant& properties); + + /* + * TODO - not yet implemented + */ + Q_INVOKABLE void removeButton(QObject* tabletButtonProxy); +public slots: + Q_INVOKABLE void close(); + +protected: + QString qmlSource() const override { return qml; } + + static std::mutex _mutex; + static std::map _fragments; +private: + QString qml; + +}; + +#endif diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index db3df34dc5..a22a5e5a5f 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -28,7 +28,7 @@ #include #include #include - +#include #include #include #include @@ -1136,6 +1136,17 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even } break; } + case QEvent::InputMethod: + case QEvent::InputMethodQuery: { + if (_quickWindow && _quickWindow->activeFocusItem()) { + event->ignore(); + if (QCoreApplication::sendEvent(_quickWindow->activeFocusItem(), event)) { + return event->isAccepted(); + } + return false; + } + break; + } #endif default: break; diff --git a/scripts/+android/defaultScripts.js b/scripts/+android/defaultScripts.js index 54c899d1da..a85b7e2208 100644 --- a/scripts/+android/defaultScripts.js +++ b/scripts/+android/defaultScripts.js @@ -13,7 +13,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", - "system/touchscreenvirtualpad.js"/*, + "system/touchscreenvirtualpad.js", + "system/bottombar.js"/*, "system/away.js", "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrabAndroid.js", diff --git a/scripts/system/bottombar.js b/scripts/system/bottombar.js new file mode 100644 index 0000000000..064025f392 --- /dev/null +++ b/scripts/system/bottombar.js @@ -0,0 +1,159 @@ +"use strict"; +// +// bottombar.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on Jan 18, 2018 +// 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() { // BEGIN LOCAL_SCOPE + +var bottombar; +var bottomHudOptionsBar; +var gotoBtn; + +var gotoScript = Script.require('./goto-android.js'); + +var logEnabled = false; + +function printd(str) { + if (logEnabled) { + print("[bottombar.js] " + str); + } +} + +function init() { + gotoScript.init(); + gotoScript.setOnShownChange(function (shown) { + if (shown) { + showAddressBar(); + } else { + hideAddressBar(); + } + }); + + setupBottomBar(); + setupBottomHudOptionsBar(); + + raiseBottomBar(); + +} + +function shutdown() { +} + +function setupBottomBar() { + bottombar = new QmlFragment({ + qml: "hifi/bottombar.qml" + }); + + bottombar.fromQml.connect(function(message) { + switch (message.method) { + case 'hide': + lowerBottomBar(); + break; + default: + print('[bottombar.js] Unrecognized message from bottomHud.qml:', JSON.stringify(message)); + } + }); + + + gotoBtn = bottombar.addButton({ + icon: "icons/goto-i.svg", + activeIcon: "icons/goto-a.svg", + bgOpacity: 0, + hoverBgOpacity: 0, + activeBgOpacity: 0, + activeHoverBgOpacity: 0, + height: 240, + width: 300, + iconSize: 108, + textSize: 45, + text: "GO TO" + }); + + gotoBtn.clicked.connect(function() { + if (!gotoScript.isVisible()) { + showAddressBar(); + } else { + hideAddressBar(); + } + }); + + // TODO: setup all the buttons or provide a dynamic interface + + raiseBottomBar(); + + +} + +var setupBottomHudOptionsBar = function() { + var bottomHud = new QmlFragment({ + qml: "hifi/bottomHudOptions.qml" + }); + + bottomHudOptionsBar = { + show: function() { + bottomHud.setVisible(true); + }, + hide: function() { + bottomHud.setVisible(false); + }, + qmlFragment: bottomHud + }; + bottomHud.fromQml.connect( + function(message) { + switch (message.method) { + case 'showUpBar': + printd('[bottombar.js] showUpBar message from bottomHudOptions.qml: ', JSON.stringify(message)); + raiseBottomBar(); + break; + default: + print('[bottombar.js] Unrecognized message from bottomHudOptions.qml:', JSON.stringify(message)); + } + } + ); +} + +function lowerBottomBar() { + if (bottombar) { + bottombar.setVisible(false); + } + if (bottomHudOptionsBar) { + bottomHudOptionsBar.show(); + } +} + +function raiseBottomBar() { + print('[bottombar.js] raiseBottomBar begin'); + if (bottombar) { + bottombar.setVisible(true); + } + if (bottomHudOptionsBar) { + bottomHudOptionsBar.hide(); + } + print('[bottombar.js] raiseBottomBar end'); +} + +function showAddressBar() { + gotoScript.show(); + gotoBtn.isActive = true; +} + +function hideAddressBar() { + gotoScript.hide(); + gotoBtn.isActive = false; +} + + + +Script.scriptEnding.connect(function () { + shutdown(); +}); + +init(); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/goto-android.js b/scripts/system/goto-android.js new file mode 100644 index 0000000000..e917455128 --- /dev/null +++ b/scripts/system/goto-android.js @@ -0,0 +1,96 @@ +"use strict"; +// +// goto-android.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on 12 Sep 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 +// + +var window; + + +var logEnabled = false; +function printd(str) { + if (logEnabled) + print("[goto-android.js] " + str); +} + +function init() { +} + +function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'shownChanged': + if (notifyShownChange) { + notifyShownChange(message.params.shown); + } ; + break; + case 'hide': + module.exports.hide(); + module.exports.onHidden(); + break; + default: + print('[goto-android.js] Unrecognized message from AddressBarDialog.qml:', JSON.stringify(message)); + } +} + +function sendToQml(message) { + window.sendToQml(message); +} + +var isVisible = false; +var notifyShownChange; +module.exports = { + init: function() { + window = new QmlFragment({ + qml: "AddressBarDialog.qml", + visible: false + }); + }, + show: function() { + Controller.setVPadEnabled(false); + if (window) { + window.fromQml.connect(fromQml); + window.setVisible(true); + isVisible = true; + } + }, + hide: function() { + Controller.setVPadEnabled(true); + if (window) { + window.fromQml.disconnect(fromQml); + window.setVisible(false); + } + isVisible = false; + }, + destroy: function() { + if (window) { + window.close(); + window = null; + } + }, + isVisible: function() { + return isVisible; + }, + width: function() { + return window ? window.size.x : 0; + }, + height: function() { + return window ? window.size.y : 0; + }, + position: function() { + return window && isVisible ? window.position : null; + }, + setOnShownChange: function(f) { + notifyShownChange = f; + }, + onHidden: function() { } + + +}; + +init(); From 7e874510e3eb11f200410c38570ea157e4d78496 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Thu, 25 Jan 2018 12:36:56 -0300 Subject: [PATCH 2/8] Fix undefined color in address bar --- interface/resources/qml/+android/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/+android/AddressBarDialog.qml b/interface/resources/qml/+android/AddressBarDialog.qml index e07af24b8d..0b12301561 100644 --- a/interface/resources/qml/+android/AddressBarDialog.qml +++ b/interface/resources/qml/+android/AddressBarDialog.qml @@ -220,7 +220,7 @@ Item { location.visible = false; } else { notice.text = AddressManager.isConnected ? "YOUR LOCATION:" : "NOT CONNECTED"; - notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.turquoise : hifiStyleConstants.colors.redHighlight; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.blueHighlight : hifiStyleConstants.colors.redHighlight; // Display hostname, which includes ip address, localhost, and other non-placenames. location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); location.visible = true; From 7fb28f4296cac4b925e1d6f010102d03814c7787 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Fri, 26 Jan 2018 17:49:08 -0300 Subject: [PATCH 3/8] Add audio bar to mute/unmute the mic --- .../resources/icons/+android/mic-mute-a.svg | 18 +++++ .../resources/icons/+android/mic-mute-i.svg | 21 ++++++ .../resources/icons/+android/mic-unmute-a.svg | 70 ++++++++++++++++++ .../resources/icons/+android/mic-unmute-i.svg | 22 ++++++ .../resources/qml/hifi/+android/AudioBar.qml | 71 +++++++++++++++++++ scripts/+android/defaultScripts.js | 5 +- scripts/system/+android/audio.js | 67 +++++++++++++++++ scripts/system/{ => +android}/bottombar.js | 2 +- .../{goto-android.js => +android/goto.js} | 0 .../{ => +android}/touchscreenvirtualpad.js | 2 +- 10 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 interface/resources/icons/+android/mic-mute-a.svg create mode 100644 interface/resources/icons/+android/mic-mute-i.svg create mode 100644 interface/resources/icons/+android/mic-unmute-a.svg create mode 100644 interface/resources/icons/+android/mic-unmute-i.svg create mode 100644 interface/resources/qml/hifi/+android/AudioBar.qml create mode 100644 scripts/system/+android/audio.js rename scripts/system/{ => +android}/bottombar.js (98%) rename scripts/system/{goto-android.js => +android/goto.js} (100%) rename scripts/system/{ => +android}/touchscreenvirtualpad.js (93%) diff --git a/interface/resources/icons/+android/mic-mute-a.svg b/interface/resources/icons/+android/mic-mute-a.svg new file mode 100644 index 0000000000..73555ce21b --- /dev/null +++ b/interface/resources/icons/+android/mic-mute-a.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/mic-mute-i.svg b/interface/resources/icons/+android/mic-mute-i.svg new file mode 100644 index 0000000000..56e33eab6b --- /dev/null +++ b/interface/resources/icons/+android/mic-mute-i.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/mic-unmute-a.svg b/interface/resources/icons/+android/mic-unmute-a.svg new file mode 100644 index 0000000000..bb28dc0f2b --- /dev/null +++ b/interface/resources/icons/+android/mic-unmute-a.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/+android/mic-unmute-i.svg b/interface/resources/icons/+android/mic-unmute-i.svg new file mode 100644 index 0000000000..76f98d7f0c --- /dev/null +++ b/interface/resources/icons/+android/mic-unmute-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/qml/hifi/+android/AudioBar.qml b/interface/resources/qml/hifi/+android/AudioBar.qml new file mode 100644 index 0000000000..f524595ef5 --- /dev/null +++ b/interface/resources/qml/hifi/+android/AudioBar.qml @@ -0,0 +1,71 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 +import Qt.labs.settings 1.0 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls +import ".." + +Item { + id: bar + x:0 + y:0 + width: 300 + height: 300 + z: -1 + + signal sendToScript(var message); + signal windowClosed(); + + property bool shown: true + + onShownChanged: { + bar.visible = shown; + } + + Rectangle { + anchors.fill : parent + color: "transparent" + Flow { + id: flowMain + spacing: 10 + flow: Flow.TopToBottom + layoutDirection: Flow.TopToBottom + anchors.fill: parent + anchors.margins: 4 + } + } + + Component.onCompleted: { + // put on bottom + x = 0; + y = 0; + width = 300; + height = 300; + } + + function addButton(properties) { + var component = Qt.createComponent("button.qml"); + if (component.status == Component.Ready) { + var button = component.createObject(flowMain); + // copy all properites to button + var keys = Object.keys(properties).forEach(function (key) { + button[key] = properties[key]; + }); + return button; + } else if( component.status == Component.Error) { + console.log("Load button errors " + component.errorString()); + } + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + +} diff --git a/scripts/+android/defaultScripts.js b/scripts/+android/defaultScripts.js index a85b7e2208..2a4af9afbf 100644 --- a/scripts/+android/defaultScripts.js +++ b/scripts/+android/defaultScripts.js @@ -13,8 +13,9 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", - "system/touchscreenvirtualpad.js", - "system/bottombar.js"/*, + "system/+android/touchscreenvirtualpad.js", + "system/+android/bottombar.js", + "system/+android/audio.js" /*, "system/away.js", "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrabAndroid.js", diff --git a/scripts/system/+android/audio.js b/scripts/system/+android/audio.js new file mode 100644 index 0000000000..b4f156d4bf --- /dev/null +++ b/scripts/system/+android/audio.js @@ -0,0 +1,67 @@ +"use strict"; +// +// audio.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on Jan 16, 2018 +// 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() { // BEGIN LOCAL_SCOPE + +var audiobar; +var audioButton; + +var logEnabled = true; + +function printd(str) { + if (logEnabled) + print("[audio.js] " + str); +} + +function init() { + audiobar = new QmlFragment({ + qml: "hifi/AudioBar.qml" + }); + + audioButton = audiobar.addButton({ + icon: "icons/mic-unmute-a.svg", + activeIcon: "icons/mic-mute-a.svg", + text: "", + bgOpacity: 0.0, + activeBgOpacity: 0.0, + bgColor: "#FFFFFF" + }); + + onMuteToggled(); + + audioButton.clicked.connect(onMuteClicked); + Audio.mutedChanged.connect(onMuteToggled); +} + +function onMuteClicked() { + printd("On Mute Clicked"); + //Menu.setIsOptionChecked("Mute Microphone", !Menu.isOptionChecked("Mute Microphone")); + Audio.muted = !Audio.muted; + onMuteToggled(); +} + +function onMuteToggled() { + printd("On Mute Toggled"); + audioButton.isActive = Audio.muted; // Menu.isOptionChecked("Mute Microphone") + printd("Audio button is active: " + audioButton.isActive); +} + +Script.scriptEnding.connect(function () { + if(audioButton) { + audioButton.clicked.disconnect(onMuteClicked); + Audio.mutedChanged.connect(onMuteToggled); + } +}); + +init(); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/bottombar.js b/scripts/system/+android/bottombar.js similarity index 98% rename from scripts/system/bottombar.js rename to scripts/system/+android/bottombar.js index 064025f392..e58840ad6f 100644 --- a/scripts/system/bottombar.js +++ b/scripts/system/+android/bottombar.js @@ -15,7 +15,7 @@ var bottombar; var bottomHudOptionsBar; var gotoBtn; -var gotoScript = Script.require('./goto-android.js'); +var gotoScript = Script.require('./goto.js'); var logEnabled = false; diff --git a/scripts/system/goto-android.js b/scripts/system/+android/goto.js similarity index 100% rename from scripts/system/goto-android.js rename to scripts/system/+android/goto.js diff --git a/scripts/system/touchscreenvirtualpad.js b/scripts/system/+android/touchscreenvirtualpad.js similarity index 93% rename from scripts/system/touchscreenvirtualpad.js rename to scripts/system/+android/touchscreenvirtualpad.js index e6f9204d4d..fa41a2b5e0 100644 --- a/scripts/system/touchscreenvirtualpad.js +++ b/scripts/system/+android/touchscreenvirtualpad.js @@ -1,6 +1,6 @@ "use strict"; // -// android.js +// touchscreenvirtualpad.js // scripts/system/ // // Created by Gabriel Calero & Cristian Duarte on Jan 16, 2018 From a5a36b4b8f63830b898f35c9aed8468ba6aecad0 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Fri, 26 Jan 2018 17:50:16 -0300 Subject: [PATCH 4/8] Don't show help on android. Hide menu bar --- interface/src/Application.cpp | 2 ++ .../src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 67c3de743e..7f4b8e6260 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2826,7 +2826,9 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (firstRun.get()) { +#if !defined(Q_OS_ANDROID) showHelp(); +#endif if (sandboxIsRunning) { qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home."; DependencyManager::get()->goToLocalSandbox(); diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index f1fefc9425..1a1714ad56 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -72,6 +72,9 @@ void Basic2DWindowOpenGLDisplayPlugin::uncustomizeContext() { bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { _framerateActions.clear(); +#if defined(Q_OS_ANDROID) + _container->setFullscreen(nullptr, true); +#endif _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), FULLSCREEN, [this](bool clicked) { if (clicked) { From 999e3a39fa06eb1146790ecc9eedb46cc7bb0f24 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Wed, 24 Jan 2018 12:29:58 -0300 Subject: [PATCH 5/8] Add mode selector (my view / radar), not finished --- .../resources/icons/+android/myview-a.svg | 56 ++++++ .../resources/icons/+android/myview-hover.svg | 54 +++++ .../resources/icons/+android/myview-i.svg | 56 ++++++ .../resources/icons/+android/radar-a.svg | 1 + .../resources/icons/+android/radar-hover.svg | 1 + .../resources/icons/+android/radar-i.svg | 1 + .../resources/qml/hifi/+android/modesbar.qml | 73 +++++++ scripts/+android/defaultScripts.js | 3 +- scripts/system/+android/modes.js | 187 ++++++++++++++++++ 9 files changed, 431 insertions(+), 1 deletion(-) create mode 100755 interface/resources/icons/+android/myview-a.svg create mode 100755 interface/resources/icons/+android/myview-hover.svg create mode 100755 interface/resources/icons/+android/myview-i.svg create mode 100755 interface/resources/icons/+android/radar-a.svg create mode 100755 interface/resources/icons/+android/radar-hover.svg create mode 100755 interface/resources/icons/+android/radar-i.svg create mode 100644 interface/resources/qml/hifi/+android/modesbar.qml create mode 100644 scripts/system/+android/modes.js diff --git a/interface/resources/icons/+android/myview-a.svg b/interface/resources/icons/+android/myview-a.svg new file mode 100755 index 0000000000..307b559c95 --- /dev/null +++ b/interface/resources/icons/+android/myview-a.svg @@ -0,0 +1,56 @@ + + + +image/svg+xmlAsset 3 \ No newline at end of file diff --git a/interface/resources/icons/+android/myview-hover.svg b/interface/resources/icons/+android/myview-hover.svg new file mode 100755 index 0000000000..49656ad294 --- /dev/null +++ b/interface/resources/icons/+android/myview-hover.svg @@ -0,0 +1,54 @@ + + + +image/svg+xmlAsset 3 \ No newline at end of file diff --git a/interface/resources/icons/+android/myview-i.svg b/interface/resources/icons/+android/myview-i.svg new file mode 100755 index 0000000000..574f51c615 --- /dev/null +++ b/interface/resources/icons/+android/myview-i.svg @@ -0,0 +1,56 @@ + + + +image/svg+xmlAsset 3 \ No newline at end of file diff --git a/interface/resources/icons/+android/radar-a.svg b/interface/resources/icons/+android/radar-a.svg new file mode 100755 index 0000000000..e4b157f827 --- /dev/null +++ b/interface/resources/icons/+android/radar-a.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/interface/resources/icons/+android/radar-hover.svg b/interface/resources/icons/+android/radar-hover.svg new file mode 100755 index 0000000000..e4b157f827 --- /dev/null +++ b/interface/resources/icons/+android/radar-hover.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/interface/resources/icons/+android/radar-i.svg b/interface/resources/icons/+android/radar-i.svg new file mode 100755 index 0000000000..3994a775d3 --- /dev/null +++ b/interface/resources/icons/+android/radar-i.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/interface/resources/qml/hifi/+android/modesbar.qml b/interface/resources/qml/hifi/+android/modesbar.qml new file mode 100644 index 0000000000..8ce455c2c1 --- /dev/null +++ b/interface/resources/qml/hifi/+android/modesbar.qml @@ -0,0 +1,73 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 +import Qt.labs.settings 1.0 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls +import ".." + +Item { + id: modesbar + y:60 + Rectangle { + anchors.fill : parent + color: "transparent" + Flow { + id: flowMain + spacing: 0 + flow: Flow.TopToBottom + layoutDirection: Flow.TopToBottom + anchors.fill: parent + anchors.margins: 4 + } + } + + Component.onCompleted: { + width = 330; + height = 330; + x=Window.innerWidth - width; + } + + function addButton(properties) { + var component = Qt.createComponent("button.qml"); + console.log("load button"); + if (component.status == Component.Ready) { + console.log("load button 2"); + var button = component.createObject(flowMain); + // copy all properites to button + var keys = Object.keys(properties).forEach(function (key) { + button[key] = properties[key]; + }); + return button; + } else if( component.status == Component.Error) { + console.log("Load button errors " + component.errorString()); + } + } + + function removeButton(name) { + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + + function fromScript(message) { + switch (message.type) { + case "allButtonsShown": + modesbar.height = flowMain.children.length * 100 + 10; + break; + case "inactiveButtonsHidden": + modesbar.height = 100 + 10; + break; + default: + break; + } + } + +} diff --git a/scripts/+android/defaultScripts.js b/scripts/+android/defaultScripts.js index 2a4af9afbf..a61c205980 100644 --- a/scripts/+android/defaultScripts.js +++ b/scripts/+android/defaultScripts.js @@ -15,7 +15,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", "system/+android/touchscreenvirtualpad.js", "system/+android/bottombar.js", - "system/+android/audio.js" /*, + "system/+android/audio.js" /, + "system/+android/modes.js"*, "system/away.js", "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrabAndroid.js", diff --git a/scripts/system/+android/modes.js b/scripts/system/+android/modes.js new file mode 100644 index 0000000000..0be53c2fc4 --- /dev/null +++ b/scripts/system/+android/modes.js @@ -0,0 +1,187 @@ +"use strict"; +// +// modes.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on Jan 23, 2018 +// 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() { // BEGIN LOCAL_SCOPE + +var modesbar; +var modesButtons; +var currentSelectedBtn; + +var SETTING_CURRENT_MODE_KEY = 'Android/Mode'; +var MODE_VR = "VR", MODE_RADAR = "RADAR", MODE_MY_VIEW = "MY VIEW"; +var DEFAULT_MODE = MODE_RADAR; +var logEnabled = true; + +function printd(str) { + if (logEnabled) { + print("[modes.js] " + str); + } +} + +function init() { + setupModesBar(); +} + +function shutdown() { + +} + +function setupModesBar() { + + var bar = new QmlFragment({ + qml: "hifi/modesbar.qml" + }); + var buttonRadarMode = bar.addButton({ + icon: "icons/radar-i.svg", + activeIcon: "icons/radar-a.svg", + hoverIcon: "icons/radar-a.svg", + activeBgOpacity: 0.0, + hoverBgOpacity: 0.0, + activeHoverBgOpacity: 0.0, + text: "RADAR", + height:240, + bottomMargin: 6, + textSize: 45 + }); + var buttonMyViewMode = bar.addButton({ + icon: "icons/myview-i.svg", + activeIcon: "icons/myview-a.svg", + hoverIcon: "icons/myview-a.svg", + activeBgOpacity: 0.0, + hoverBgOpacity: 0.0, + activeHoverBgOpacity: 0.0, + text: "MY VIEW", + height: 240, + bottomMargin: 6, + textSize: 45 + }); + + modesButtons = [buttonRadarMode, buttonMyViewMode]; + + var mode = getCurrentModeSetting(); + + var buttonsRevealed = false; + bar.sendToQml({type: "inactiveButtonsHidden"}); + + modesbar = { + restoreMyViewButton: function() { + switchModeButtons(buttonMyViewMode); + saveCurrentModeSetting(MODE_MY_VIEW); + }, + sendToQml: function(o) { bar.sendToQml(o); }, + qmlFragment: bar + }; + + buttonRadarMode.clicked.connect(function() { + //if (connections.isVisible()) return; + saveCurrentModeSetting(MODE_RADAR); + printd("Radar clicked"); + onButtonClicked(buttonRadarMode, function() { + //radar.startRadarMode(); + }); + }); + buttonMyViewMode.clicked.connect(function() { + //if (connections.isVisible()) return; + saveCurrentModeSetting(MODE_MY_VIEW); + printd("My View clicked"); + onButtonClicked(buttonMyViewMode, function() { + if (currentSelectedBtn == buttonRadarMode) { + //radar.endRadarMode(); + } + }); + }); + + var savedButton; + if (mode == MODE_MY_VIEW) { + savedButton = buttonMyViewMode; + } else { + savedButton = buttonRadarMode; + } + printd("[MODE] previous mode " + mode); + + savedButton.clicked(); +} + +function saveCurrentModeSetting(mode) { + Settings.setValue(SETTING_CURRENT_MODE_KEY, mode); +} + +function getCurrentModeSetting(mode) { + return Settings.getValue(SETTING_CURRENT_MODE_KEY, DEFAULT_MODE); +} + +function showAllButtons() { + for (var i=0; i Date: Wed, 31 Jan 2018 15:12:44 -0300 Subject: [PATCH 6/8] Fix +android/defaultScripts array of filenames. --- scripts/+android/defaultScripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/+android/defaultScripts.js b/scripts/+android/defaultScripts.js index a61c205980..a8f6bf42a1 100644 --- a/scripts/+android/defaultScripts.js +++ b/scripts/+android/defaultScripts.js @@ -15,8 +15,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", "system/+android/touchscreenvirtualpad.js", "system/+android/bottombar.js", - "system/+android/audio.js" /, - "system/+android/modes.js"*, + "system/+android/audio.js" , + "system/+android/modes.js"/*, "system/away.js", "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrabAndroid.js", From cc361385d9fcad90039773e2bc11e737dd0f40e5 Mon Sep 17 00:00:00 2001 From: Cristian Luis Duarte Date: Wed, 31 Jan 2018 15:33:14 -0300 Subject: [PATCH 7/8] Android - Make it possible to use the touchpad for walk/fly (and orientate/see) when not in independent camera mode (allowing first person and third person). --- .../resources/controllers/touchscreenvirtualpad.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/interface/resources/controllers/touchscreenvirtualpad.json b/interface/resources/controllers/touchscreenvirtualpad.json index 9f65994e98..8c21044c3b 100644 --- a/interface/resources/controllers/touchscreenvirtualpad.json +++ b/interface/resources/controllers/touchscreenvirtualpad.json @@ -1,15 +1,12 @@ { "name": "TouchscreenVirtualPad to Actions", "channels": [ - { "from": "TouchscreenVirtualPad.LY", "when": "Application.CameraFirstPerson", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateZ" }, - { "from": "TouchscreenVirtualPad.LX", "when": "Application.CameraFirstPerson", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" }, + { "from": "TouchscreenVirtualPad.LY", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateZ" }, + { "from": "TouchscreenVirtualPad.LX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" }, - { "from": "TouchscreenVirtualPad.RX", "when": "Application.CameraFirstPerson", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.Yaw" }, + { "from": "TouchscreenVirtualPad.RX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.Yaw" }, - { "from": "TouchscreenVirtualPad.RY", - "when": "Application.CameraFirstPerson", - "to": "Actions.Pitch" - } + { "from": "TouchscreenVirtualPad.RY", "when": "!Application.CameraIndependent", "to": "Actions.Pitch" } ] } From 5a8e0d5bcb07742ce1d735df2c02f1b967b2b504 Mon Sep 17 00:00:00 2001 From: Cristian Luis Duarte Date: Fri, 2 Feb 2018 12:57:41 -0300 Subject: [PATCH 8/8] Android - Radar mode implemented. --- scripts/system/+android/modes.js | 37 +- scripts/system/+android/radar.js | 1212 ++++++++++++++++++++++++ scripts/system/+android/uniqueColor.js | 46 + 3 files changed, 1293 insertions(+), 2 deletions(-) create mode 100644 scripts/system/+android/radar.js create mode 100644 scripts/system/+android/uniqueColor.js diff --git a/scripts/system/+android/modes.js b/scripts/system/+android/modes.js index 0be53c2fc4..b29548094f 100644 --- a/scripts/system/+android/modes.js +++ b/scripts/system/+android/modes.js @@ -20,6 +20,9 @@ var MODE_VR = "VR", MODE_RADAR = "RADAR", MODE_MY_VIEW = "MY VIEW"; var DEFAULT_MODE = MODE_RADAR; var logEnabled = true; +var radar = Script.require('./radar.js'); +var uniqueColor = Script.require('./uniqueColor.js'); + function printd(str) { if (logEnabled) { print("[modes.js] " + str); @@ -27,7 +30,10 @@ function printd(str) { } function init() { + radar.setUniqueColor(uniqueColor); + radar.init(); setupModesBar(); + radar.isTouchValid = isRadarModeValidTouch; } function shutdown() { @@ -85,7 +91,7 @@ function setupModesBar() { saveCurrentModeSetting(MODE_RADAR); printd("Radar clicked"); onButtonClicked(buttonRadarMode, function() { - //radar.startRadarMode(); + radar.startRadarMode(); }); }); buttonMyViewMode.clicked.connect(function() { @@ -94,7 +100,7 @@ function setupModesBar() { printd("My View clicked"); onButtonClicked(buttonMyViewMode, function() { if (currentSelectedBtn == buttonRadarMode) { - //radar.endRadarMode(); + radar.endRadarMode(); } }); }); @@ -177,6 +183,33 @@ function onButtonClicked(clickedButton, whatToDo, hideAllAfter) { } } +function isRadarModeValidTouch(coords) { + var qmlFragments = [modesbar.qmlFragment]; + var windows = []; + for (var i=0; i < qmlFragments.length; i++) { + var aQmlFrag = qmlFragments[i]; + if (aQmlFrag != null && aQmlFrag.isVisible() && + coords.x >= aQmlFrag.position.x * 3 && coords.x <= aQmlFrag.position.x * 3 + aQmlFrag.size.x * 3 && + coords.y >= aQmlFrag.position.y * 3 && coords.y <= aQmlFrag.position.y * 3 + aQmlFrag.size.y * 3 + ) { + printd("godViewModeTouchValid- false because of qmlFragments!? idx " + i); + return false; + } + } + + for (var i=0; i < windows.length; i++) { + var aWin = windows[i]; + if (aWin != null && aWin.position() != null && + coords.x >= aWin.position().x * 3 && coords.x <= aWin.position().x * 3 + aWin.width() * 3 && + coords.y >= aWin.position().y * 3 && coords.y <= aWin.position().y * 3 + aWin.height() * 3 + ) { + printd("godViewModeTouchValid- false because of windows!?"); + return false; + } + } + printd("godViewModeTouchValid- true by default "); + return true; +} Script.scriptEnding.connect(function () { shutdown(); diff --git a/scripts/system/+android/radar.js b/scripts/system/+android/radar.js new file mode 100644 index 0000000000..9fa07178f6 --- /dev/null +++ b/scripts/system/+android/radar.js @@ -0,0 +1,1212 @@ +"use strict"; +// +// radar.js +// scripts/system/+android/ +// +// Created by Cristian Duarte & Gabriel Calero on 31 Jan 2018 +// 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 +// + +var radarModeInterface = {}; + +var logEnabled = true; +function printd(str) { + if (logEnabled) + print("[radar.js] " + str); +} + +var radar = false; +var radarHeight = 10; // camera position meters above the avatar +var tablet; + +var RADAR_CAMERA_OFFSET = -1; // 1 meter below the avatar +var ABOVE_GROUND_DROP = 2; +var MOVE_BY = 1; + +// Swipe/Drag vars +var PINCH_INCREMENT_FIRST = 0.4; // 0.1 meters zoom in - out +var PINCH_INCREMENT = 0.4; // 0.1 meters zoom in - out +var RADAR_HEIGHT_MAX_PLUS_AVATAR = 40; +var RADAR_HEIGHT_MIN_PLUS_AVATAR = 2; +var RADAR_CAMERA_DISTANCE_TO_ICONS = 0.5; // Icons are near the camera to prevent the LOD manager dismissing them +var RADAR_ICONS_APPARENT_DISTANCE_TO_AVATAR_BASE = 1; // How much above the avatar base should the icon appear +var AVATAR_DISPLAY_NAME_HEIGHT = 38; +var AVATAR_DISPLAY_NAME_CHAR_WIDTH = 18; +var lastDragAt; +var lastDeltaDrag; + +var uniqueColor; + +function moveTo(position) { + if (radar) { + MyAvatar.position = position; + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: radarHeight, z: 0}); + } +} + +function keyPressEvent(event) { + if (radar) { + switch(event.text) { + case "UP": + moveTo(Vec3.sum(MyAvatar.position, {x:0.0, y: 0, z: -1 * MOVE_BY})); + break; + case "DOWN": + moveTo(Vec3.sum(MyAvatar.position, {x:0, y: 0, z: MOVE_BY})); + break; + case "LEFT": + moveTo(Vec3.sum(MyAvatar.position, {x:-1 * MOVE_BY, y: 0, z: 0})); + break; + case "RIGHT": + moveTo(Vec3.sum(MyAvatar.position, {x:MOVE_BY, y: 0, z: 0})); + break; + } + } +} + +function actionOnObjectFromEvent(event) { + var rayIntersection = findRayIntersection(Camera.computePickRay(event.x, event.y)); + if (rayIntersection && rayIntersection.intersects && rayIntersection.overlayID) { + printd("found overlayID touched " + rayIntersection.overlayID); + if (entitiesByOverlayID[rayIntersection.overlayID]) { + var entity = Entities.getEntityProperties(entitiesByOverlayID[rayIntersection.overlayID], ["sourceUrl"]); + App.openUrl(entity.sourceUrl); + return true; + } + } + if (rayIntersection && rayIntersection.intersects && rayIntersection.entityID && rayIntersection.properties) { + printd("found " + rayIntersection.entityID + " of type " + rayIntersection.properties.type) ; + if (rayIntersection.properties.type == "Web") { + printd("found web element to " + rayIntersection.properties.sourceUrl); + App.openUrl(rayIntersection.properties.sourceUrl); + return true; + } + } + return false; +} + +function mousePress(event) { + if (!isTouchValid(coords)) { + currentTouchIsValid = false; + return; + } else { + currentTouchIsValid = true; + } + mousePressOrTouchEnd(event); +} + +function mousePressOrTouchEnd(event) { + if (!currentTouchIsValid) { + return; + } + if (radar) { + if (actionOnObjectFromEvent(event)) return; + } +} + +function toggleRadarMode() { + if (radar) { + endRadar(); + } else { + startRadar(); + } +} + +function fakeDoubleTap(event) { + // CLD - temporarily disable toggling mode through double tap + // * As we have a new UI for toggling between modes, it may be discarded completely in the future. + // toggleRadarMode(); + teleporter.dragTeleportUpdate(event); + teleporter.dragTeleportRelease(event); +} + +var currentTouchIsValid = false; // Currently used to know if touch hasn't started on a UI overlay + +var DOUBLE_TAP_TIME = 300; +var fakeDoubleTapStart = Date.now(); +var touchEndCount = 0; + +/* Counts touchEnds and if there were 2 in the DOUBLE_TAP_TIME lapse, it triggers a fakeDoubleTap and returns true. + Otherwise, returns false (no double tap yet) */ +function analyzeDoubleTap(event) { + var fakeDoubleTapEnd = Date.now(); + var elapsed = fakeDoubleTapEnd - fakeDoubleTapStart; + if (elapsed > DOUBLE_TAP_TIME) { + touchEndCount = 0; + } + + // if this is our first "up" then record time so we can + // later determine if second "up" is a double tap + if (touchEndCount == 0) { + fakeDoubleTapStart = Date.now(); + } + touchEndCount++; + + if (touchEndCount >= 2) { + var fakeDoubleTapEnd = Date.now(); + var elapsed = fakeDoubleTapEnd - fakeDoubleTapStart; + printd("-- fakeDoubleTapEnd:" + fakeDoubleTapEnd + "-- elapsed:" + elapsed) + if (elapsed <= DOUBLE_TAP_TIME) { + touchEndCount = 0; + fakeDoubleTap(event); + return true; // don't do the normal touch end processing + } + + touchEndCount = 0; + } + return false; +} + +function touchEnd(event) { + printd("touchEnd received " + JSON.stringify(event)); + // Clean up touch variables + lastDragAt = null; + lastDeltaDrag = null; + touchStartingCoordinates = null; // maybe in special cases it should be setup later? + startedDraggingCamera = false; + prevTouchPinchRadius = null; + draggingCamera = false; + + if (movingCamera) { + // if camera was indeed moving, we should not further process, it was just dragging + movingCamera = false; + dragModeFunc = null; + return; + } + + // teleport release analysis + if (teleporter && teleporter.dragTeleportUpdate == dragModeFunc) { + teleporter.dragTeleportRelease(event); + dragModeFunc = null; + return; + } + dragModeFunc = null; + + // if pinching or moving is still detected, cancel + if (event.isPinching) { printd("touchEnd fail because isPinching");return;} + if (event.isPinchOpening) { printd("touchEnd fail because isPinchingOpening");return;} + if (event.isMoved) { printd("touchEnd fail because isMoved");return;} + + // if touch is invalid, cancel + if (!currentTouchIsValid) { printd("touchEnd fail because !currentTouchIsValid");return;} + + if (analyzeDoubleTap(event)) return; // double tap detected, finish + + if (radar) { + mousePressOrTouchEnd(event); + } +} + +/** +* Polyfill for sign(x) +*/ +if (!Math.sign) { + Math.sign = function(x) { + // If x is NaN, the result is NaN. + // If x is -0, the result is -0. + // If x is +0, the result is +0. + // If x is negative and not -0, the result is -1. + // If x is positive and not +0, the result is +1. + x = +x; // convert to a number + if (x === 0 || isNaN(x)) { + return Number(x); + } + return x > 0 ? 1 : -1; + }; +} + +/******************************************************************************************************** + * Line and Plane intersection methods + ********************************************************************************************************/ + +/** +* findLinePlaneIntersection +* Given points p {x: y: z:} and q that define a line, and the plane +* of formula ax+by+cz+d = 0, returns the intersection point or null if none. +*/ +function findLinePlaneIntersection(p, q, a, b, c, d) { + return findLinePlaneIntersectionCoords(p.x, p.y, p.z, q.x, q.y, q.z, a, b, c, d); +} + +/** +* findLineToHeightIntersection +* Given points p {x: y: z:} and q that define a line, and a planeY +* value that defines a plane paralel to 'the floor' xz plane, +* returns the intersection to that plane or null if none. +*/ +function findLineToHeightIntersection(p, q, planeY) { + return findLinePlaneIntersection(p, q, 0, 1, 0, -planeY); +} + +/** +* findLinePlaneIntersectionCoords (to avoid requiring unnecessary instantiation) +* Given points p with px py pz and q that define a line, and the plane +* of formula ax+by+cz+d = 0, returns the intersection point or null if none. +*/ +function findLinePlaneIntersectionCoords(px, py, pz, qx, qy, qz, a, b, c, d) { + var tDenom = a*(qx-px) + b*(qy-py) + c*(qz-pz); + if (tDenom == 0) return null; + + var t = - ( a*px + b*py + c*pz + d ) / tDenom; + + return { + x: (px+t*(qx-px)), + y: (py+t*(qy-py)), + z: (pz+t*(qz-pz)) + }; +} + +/** +* findLineToHeightIntersection +* Given points p with px py pz and q that define a line, and a planeY +* value that defines a plane paralel to 'the floor' xz plane, +* returns the intersection to that plane or null if none. +*/ +function findLineToHeightIntersectionCoords(px, py, pz, qx, qy, qz, planeY) { + return findLinePlaneIntersectionCoords(px, py, pz, qx, qy, qz, 0, 1, 0, -planeY); +} + +function findRayIntersection(pickRay) { + // Check 3D overlays and entities. Argument is an object with origin and direction. + var result = Overlays.findRayIntersection(pickRay); + if (!result.intersects) { + result = Entities.findRayIntersection(pickRay, true); + } + return result; +} + +/** + * Given a 2d point (x,y) this function returns the intersection (x, y, z) + * of the computedPickRay for that point with the plane y = py + */ +function computePointAtPlaneY(x,y,py) { + var ray = Camera.computePickRay(x, y); + var p1=ray.origin; + var p2=Vec3.sum(p1, Vec3.multiply(ray.direction, 1)); + return findLineToHeightIntersectionCoords(p1.x, p1.y, p1.z, + p2.x, p2.y, p2.z, py); +} + +/******************************************************************************************************** + * + ********************************************************************************************************/ + +function isTouchValid(coords) { + // TODO: Extend to the detection of touches on new menu bars + var radarModeTouchValid = radarModeInterface.isTouchValid(coords); + + // getItemAtPoint does not exist anymore, look for another way to know if we are touching buttons + // is it still needed? + return /*!tablet.getItemAtPoint(coords) && */radarModeTouchValid; +} + +/******************************************************************************************************** + * + ********************************************************************************************************/ + +var touchStartingCoordinates = null; + +var KEEP_PRESSED_FOR_TELEPORT_MODE_TIME = 750; +var touchBeginTime; + +function touchBegin(event) { + var coords = { x: event.x, y: event.y }; + if (!isTouchValid(coords) ) { + printd("analyze touch - RADAR_TOUCH - INVALID"); + currentTouchIsValid = false; + touchStartingCoordinates = null; + } else { + printd("analyze touch - RADAR_TOUCH - ok"); + currentTouchIsValid = true; + touchStartingCoordinates = coords; + touchBeginTime = Date.now(); + } +} + +var startedDraggingCamera = false; // first time +var draggingCamera = false; // is trying +var movingCamera = false; // definitive + +var MIN_DRAG_DISTANCE_TO_CONSIDER = 100; // distance by axis, not real distance + +var prevTouchPinchRadius = null; + +function pinchUpdate(event) { + if (!event.isMoved) return; + if (event.radius <= 0) return; + + // pinch management + var avatarY = MyAvatar.position.y; + var pinchIncrement; + if (!!prevTouchPinchRadius) { + // no prev value + pinchIncrement = PINCH_INCREMENT * Math.abs(event.radius - prevTouchPinchRadius) * 0.1; + } else { + pinchIncrement = PINCH_INCREMENT_FIRST; + } + + if (event.isPinching) { + if (radarHeight + pinchIncrement > RADAR_HEIGHT_MAX_PLUS_AVATAR + avatarY) { + radarHeight = RADAR_HEIGHT_MAX_PLUS_AVATAR + avatarY; + } else { + radarHeight += pinchIncrement; + } + } else if (event.isPinchOpening) { + if (radarHeight - pinchIncrement < RADAR_HEIGHT_MIN_PLUS_AVATAR + avatarY) { + radarHeight = RADAR_HEIGHT_MIN_PLUS_AVATAR + avatarY; + } else { + radarHeight -= pinchIncrement; + } + } + var deltaHeight = avatarY + radarHeight - Camera.position.y; + Camera.position = Vec3.sum(Camera.position, {x:0, y: deltaHeight, z: 0}); + if (!draggingCamera) { + startedDraggingCamera = true; + draggingCamera = true; + } + + prevTouchPinchRadius = event.radius; +} + +function isInsideSquare(coords0, coords1, halfside) { + return Math.abs(coords0.x-coords1.x) <= halfside && Math.abs(coords0.y-coords1.y) <= halfside; +} + +function dragScrollUpdate(event) { + if (!event.isMoved) return; + + // drag management + var pickRay = Camera.computePickRay(event.x, event.y); + var dragAt = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, radarHeight)); + + if (lastDragAt === undefined || lastDragAt === null) { + lastDragAt = dragAt; + return; + } + + var deltaDrag = {x: (lastDragAt.x - dragAt.x), y: 0, z: (lastDragAt.z-dragAt.z)}; + + lastDragAt = dragAt; + if (lastDeltaDrag === undefined || lastDeltaDrag === null) { + lastDeltaDrag = deltaDrag; + return; + } + + if (!draggingCamera) { + startedDraggingCamera = true; + draggingCamera = true; + } else { + if (!movingCamera) { + if (!isInsideSquare(touchStartingCoordinates, event, MIN_DRAG_DISTANCE_TO_CONSIDER)) { + movingCamera = true; + } + } + + if (movingCamera) { + if (Math.sign(deltaDrag.x) == Math.sign(lastDeltaDrag.x) && Math.sign(deltaDrag.z) == Math.sign(lastDeltaDrag.z)) { + // Process movement if direction of the movement is the same than the previous frame + // process delta + var moveCameraTo = Vec3.sum(Camera.position, deltaDrag); + // move camera + Camera.position = moveCameraTo; + } else { + // Do not move camera if it's changing direction in this case, wait until the next direction confirmation.. + } + lastDeltaDrag = deltaDrag; // save last + } + } +} + +/******************************************************************************************************** + * Teleport feature + ********************************************************************************************************/ + +function Teleporter() { + + var SURFACE_DETECTION_FOR_TELEPORT = true; // true if uses teleport.js similar logic to detect surfaces. false if uses plain teleport to avatar same height. + + var TELEPORT_TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); + var TELEPORT_TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); + + var TELEPORT_MODEL_DEFAULT_DIMENSIONS = { + x: 0.10, + y: 0.00001, + z: 0.10 + }; + + var teleportOverlay = Overlays.addOverlay("model", { + url: TELEPORT_TARGET_MODEL_URL, + dimensions: TELEPORT_MODEL_DEFAULT_DIMENSIONS, + orientation: Quat.fromPitchYawRollDegrees(0,180,0), + visible: false + }); + + var teleportCancelOverlay = Overlays.addOverlay("model", { + url: TELEPORT_TOO_CLOSE_MODEL_URL, + dimensions: TELEPORT_MODEL_DEFAULT_DIMENSIONS, + orientation: Quat.fromPitchYawRollDegrees(0,180,0), + visible: false + }); + + var TELEPORT_COLOR = { red: 0, green: 255, blue: 255}; + var TELEPORT_CANCEL_COLOR = { red: 255, green: 255, blue: 0}; + + var teleportLine = Overlays.addOverlay("line3d", { + start: { x: 0, y: 0, z:0 }, + end: { x: 0, y: 0, z: 0 }, + color: TELEPORT_COLOR, + alpha: 1, + lineWidth: 2, + dashed: false, + visible: false + }); + + // code from teleport.js + var TELEPORT_TARGET = { + NONE: 'none', // Not currently targetting anything + INVISIBLE: 'invisible', // The current target is an invvsible surface + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat', // The current target is a seat + } + + var TELEPORT_CANCEL_RANGE = 1; + var teleportTargetType = TELEPORT_TARGET.NONE; + + function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + + /* + * Enhanced with intersection with terrain instead of using current avatar y position if SURFACE_DETECTION_FOR_TELEPORT is true + */ + function computeDestination(touchEventPos, avatarPosition, cameraPosition, radarH) { + if (SURFACE_DETECTION_FOR_TELEPORT) { + var pickRay = Camera.computePickRay(touchEventPos.x, touchEventPos.y); + printd("newTeleportDetect - pickRay " + JSON.stringify(pickRay)); + var destination = Entities.findRayIntersection(pickRay, true, [], [], false, true); + printd("newTeleportDetect - destination " + JSON.stringify(destination)); + return destination; + } else { + var pickRay = Camera.computePickRay(touchEventPos.x, touchEventPos.y); + var pointingAt = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, radarH)); + var destination = { x: pointingAt.x, y: avatarPosition.y, z: pointingAt.z }; + return destination; + } + } + + function renderTeleportOverlays(destination) { + var overlayPosition = findLineToHeightIntersection(destination, Camera.position, Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS); + printd("[newTeleport] TELEPORT ! render overlay at " + JSON.stringify(overlayPosition)); + + // CLD note Oct 11, 2017 + // Version of teleport.js 3c109f294f88ba7573bd1221f907f2605893c509 doesn't allow invisible surfaces, let's allow it for now + if (teleportTargetType == TELEPORT_TARGET.SURFACE || teleportTargetType == TELEPORT_TARGET.INVISIBLE) { + Overlays.editOverlay(teleportOverlay, { visible: true, position: overlayPosition }); + Overlays.editOverlay(teleportCancelOverlay, { visible: false }); + Overlays.editOverlay(teleportLine, { start: MyAvatar.position, end: destination, color: TELEPORT_COLOR, visible: true }); + } else if (teleportTargetType == TELEPORT_TARGET.INVALID) { + Overlays.editOverlay(teleportOverlay, { visible: false}); + Overlays.editOverlay(teleportCancelOverlay, { visible: true, position: overlayPosition }); + Overlays.editOverlay(teleportLine, { start: MyAvatar.position, end: destination, color: TELEPORT_CANCEL_COLOR, visible: true }); + } else { // TELEPORT_TARGET:NONE? + Overlays.editOverlay(teleportOverlay, { visible: false }); + Overlays.editOverlay(teleportCancelOverlay, { visible: false }); + Overlays.editOverlay(teleportLine, { visible: false }); + } + } + + var BORDER_DISTANCE_PX = 100; + var border_top = 0; + var border_left = 0; + var border_right = Window.innerWidth; + var border_bottom = Window.innerHeight; + + function moveOnBorders(event) { + var xDelta = 0; + var zDelta = 0; + + if (event.y <= border_top + BORDER_DISTANCE_PX) { + zDelta = -0.1; + } else if (event.y >= border_bottom - BORDER_DISTANCE_PX) { + zDelta = 0.1; + } + if (event.x <= border_left + BORDER_DISTANCE_PX) { + xDelta = -0.1; + } else if (event.x >= border_right - BORDER_DISTANCE_PX) { + xDelta = 0.1; + } + if (xDelta == 0 && zDelta == 0) { + draggingCamera = false; + return; + } + + Camera.position = Vec3.sum(Camera.position, {x:xDelta, y: 0, z: zDelta}); + draggingCamera = true; + } + + // When determininig whether you can teleport to a location, the normal of the + // point that is being intersected with is looked at. If this normal is more + // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then + // you can't teleport there. + const MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; + function getTeleportTargetType(intersection) { + if (SURFACE_DETECTION_FOR_TELEPORT) { + if (!intersection.intersects) { + return TELEPORT_TARGET.NONE; + } + var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + return TELEPORT_TARGET.SEAT; + } + + if (!props.visible) { + return TELEPORT_TARGET.INVISIBLE; + } + + var surfaceNormal = intersection.surfaceNormal; + var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z); + var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI); + + if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || + angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || + Vec3.distance(MyAvatar.position, intersection.intersection) <= TELEPORT_CANCEL_RANGE) { + return TELEPORT_TARGET.INVALID; + } else { + return TELEPORT_TARGET.SURFACE; + } + } else { + var destination = intersection; + if (Vec3.distance(MyAvatar.position, destination) <= TELEPORT_CANCEL_RANGE) { + return TELEPORT_TARGET.INVALID; + } else { + return TELEPORT_TARGET.SURFACE; + } + } + }; + + function moveToFromEvent(event) { + var destination = computeDestination(event, MyAvatar.position, Camera.position, radarHeight); + moveTo(SURFACE_DETECTION_FOR_TELEPORT? + Vec3.sum(destination.intersection, {y: 1}) + :destination); + return true; + } + + return { + dragTeleportBegin : function(event) { + printd("[newTeleport] TELEPORT began"); + var overlayDimensions = entityIconModelDimensions(); + //var destination = computeDestination(event, MyAvatar.position, Camera.position, radarHeight); + // Dimension teleport and cancel overlays (not show them yet) + Overlays.editOverlay(teleportOverlay, { dimensions: overlayDimensions }); + Overlays.editOverlay(teleportCancelOverlay, { dimensions: overlayDimensions }); + // Position line + Overlays.editOverlay(teleportLine, { visible: true, start: 0, end: 0 }); + }, + + dragTeleportUpdate : function(event) { + // if in border, move camera + moveOnBorders(event); + + var destination = computeDestination(event, MyAvatar.position, Camera.position, radarHeight); + + teleportTargetType = getTeleportTargetType(destination); + renderTeleportOverlays( SURFACE_DETECTION_FOR_TELEPORT? + destination.intersection: + destination); + }, + + dragTeleportRelease : function (event) { + printd("[newTeleport] TELEPORT released at " + JSON.stringify(event)); + // CLD note Oct 11, 2017 + // Version of teleport.js 3c109f294f88ba7573bd1221f907f2605893c509 doesn't allow invisible surfaces, let's allow it for now + if (teleportTargetType == TELEPORT_TARGET.SURFACE || teleportTargetType == TELEPORT_TARGET.INVISIBLE) { + moveToFromEvent(event); + } + teleportTargetType = TELEPORT_TARGET.NONE; + + Overlays.editOverlay(teleportOverlay, { visible: false }); + Overlays.editOverlay(teleportLine, { visible: false }); + Overlays.editOverlay(teleportCancelOverlay, { visible: false }); + } + }; + +} + +var teleporter = Teleporter(); + +/******************************************************************************************************** + * + ********************************************************************************************************/ + +var dragModeFunc = null; // by default is nothing + +function oneFingerTouchUpdate(event) { + if (dragModeFunc) { + dragModeFunc(event); + } else { + if (!isInsideSquare(touchStartingCoordinates, event, MIN_DRAG_DISTANCE_TO_CONSIDER)) { + dragModeFunc = dragScrollUpdate; + dragModeFunc(event); + } else { + var now = Date.now(); // check time + if (now - touchBeginTime >= KEEP_PRESSED_FOR_TELEPORT_MODE_TIME) { + teleporter.dragTeleportBegin(event); + dragModeFunc = teleporter.dragTeleportUpdate; + dragModeFunc(event); + } else { + // not defined yet, let's wait for time or movement to happen + } + } + } +} + +function touchUpdate(event) { + if (!currentTouchIsValid) { + return; // avoid moving and zooming when tap is over UI entities + } + if (event.isPinching || event.isPinchOpening) { + pinchUpdate(event); + } else { + oneFingerTouchUpdate(event); + } +} + +/******************************************************************************************************** + * Avatar cache structure for showing avatars markers + ********************************************************************************************************/ + +// by QUuid +var avatarsData = {}; +var avatarsIcons = []; // a parallel list of icons (overlays) to easily run through +var avatarsNames = []; // a parallel list of names (overlays) to easily run through + +function getAvatarIconForUser(uid) { + var color = uniqueColor.getColor(uid); + if (color.charAt(0) == '#' ) { + color = color.substring(1, color.length); + } + // FIXME: this is a temporary solution until we can use circle3d with lineWidth + return Script.resolvePath("assets/images/circle-"+color+".svg"); +} + +var avatarIconDimensionsVal = { x: 0, y: 0, z: 0.00001}; +function avatarIconPlaneDimensions() { + // given the current height, give a size + var xy = -0.003531 * radarHeight + 0.1; + avatarIconDimensionsVal.x = Math.abs(xy); + avatarIconDimensionsVal.y = Math.abs(xy); + // reuse object + return avatarIconDimensionsVal; +} + +function currentOverlayIconForAvatar(QUuid) { + if (avatarsData[QUuid] != undefined) { + return avatarsData[QUuid].icon; + } else { + return null; + } +} + +function currentOverlayNameForAvatar(QUuid) { + if (avatarsData[QUuid] != undefined) { + return avatarsData[QUuid].name; + } else { + return null; + } +} + +function saveAvatarData(QUuid) { + if (QUuid == null) return; + var avat = AvatarList.getAvatar(QUuid); + printd("avatar added save avatar " + QUuid); + + if (!avat) return; + + if (avatarsData[QUuid] != undefined) { + avatarsData[QUuid].position = avat.position; + } else { + var avatarIcon = Overlays.addOverlay("image3d", { + subImage: { x: 0, y: 0, width: 150, height: 142}, + url: getAvatarIconForUser(QUuid), + dimensions: ICON_ENTITY_DEFAULT_DIMENSIONS, + visible: false, + ignoreRayIntersection: false, + orientation: Quat.fromPitchYawRollDegrees(-90,0,0) + }); + + var needRefresh = !avat || !avat.displayName; + var displayName = avat && avat.displayName ? avat.displayName : "Unknown"; + var textWidth = displayName.length * AVATAR_DISPLAY_NAME_CHAR_WIDTH; + var avatarName = Overlays.addOverlay("text", { + width: textWidth, + height: AVATAR_DISPLAY_NAME_HEIGHT, + color: { red: 255, green: 255, blue: 255}, + backgroundAlpha: 0.0, + textRaiseColor: { red: 0, green: 0, blue: 0}, + font: {size: 68, bold: true}, + visible: false, + text: displayName, + textAlignCenter: true + }); + avatarsIcons.push(avatarIcon); + avatarsNames.push(avatarName); + avatarsData[QUuid] = { position: avat.position, icon: avatarIcon, name: avatarName, textWidth: textWidth, needRefresh: needRefresh }; + } +} + +function removeAvatarData(QUuid) { + if (QUuid == null) return; + + var itsOverlay = currentOverlayIconForAvatar(QUuid); + if (itsOverlay != null) { + Overlays.deleteOverlay(itsOverlay); + } + var itsNameOverlay = currentOverlayNameForAvatar(QUuid); + if (itsNameOverlay != null) { + Overlays.deleteOverlay(itsNameOverlay); + } + + var idx = avatarsIcons.indexOf(itsOverlay); + avatarsIcons.splice(idx, 1); + idx = avatarsNames.indexOf(itsNameOverlay); + avatarsNames.splice(idx, 1); + + delete avatarsData[QUuid]; +} + +function saveAllOthersAvatarsData() { + var avatarIds = AvatarList.getAvatarIdentifiers(); + var len = avatarIds.length; + for (var i = 0; i < len; i++) { + if (avatarIds[i]) { + saveAvatarData(avatarIds[i]); + } + } +} + + +function avatarAdded(QUuid) { + printd("avatar added " + QUuid);// + " at " + JSON.stringify(AvatarList.getAvatar(QUuid).position)); + saveAvatarData(QUuid); +} + +function avatarRemoved(QUuid) { + printd("avatar removed " + QUuid); + removeAvatarData(QUuid); +} + +/******************************************************************************************************** + * Avatar Icon/Markers rendering + ********************************************************************************************************/ +var myAvatarIcon; +var myAvatarName; + +function renderMyAvatarIcon() { + var iconPos = findLineToHeightIntersectionCoords( MyAvatar.position.x, + MyAvatar.position.y + RADAR_ICONS_APPARENT_DISTANCE_TO_AVATAR_BASE, + MyAvatar.position.z, + Camera.position.x, Camera.position.y, Camera.position.z, + Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS); + if (!iconPos) { printd("avatarmy icon pos null"); return;} + var iconDimensions = avatarIconPlaneDimensions(); + + var avatarPos = MyAvatar.position; + var cameraPos = Camera.position; + var commonY = Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS; + var borderPoints = [ + computePointAtPlaneY(0, 0, commonY), + computePointAtPlaneY(Window.innerWidth, Window.innerHeight, commonY) + ]; + + var p1 = findLineToHeightIntersectionCoords(avatarPos.x, avatarPos.y, avatarPos.z, + cameraPos.x, cameraPos.y, cameraPos.z, + commonY); + var x = (p1.x - borderPoints[0].x) * (Window.innerWidth) / (borderPoints[1].x - borderPoints[0].x); + var y = (p1.z - borderPoints[0].z) * (Window.innerHeight) / (borderPoints[1].z - borderPoints[0].z); + + if (!myAvatarIcon && MyAvatar.sessionUUID) { + myAvatarIcon = Overlays.addOverlay("image3d", { + subImage: { x: 0, y: 0, width: 150, height: 142}, + url: getAvatarIconForUser(MyAvatar.sessionUUID), + dimensions: ICON_ENTITY_DEFAULT_DIMENSIONS, + visible: false, + ignoreRayIntersection: false, + orientation: Quat.fromPitchYawRollDegrees(-90,0,0) + }); + } + + if (!myAvatarName) { + myAvatarName = Overlays.addOverlay("text", { + width: 40, + height: AVATAR_DISPLAY_NAME_HEIGHT, + textAlignCenter: true, + color: { red: 255, green: 255, blue: 255}, + backgroundAlpha: 0.0, + font: {size: 68, bold: true}, + textRaiseColor: { red: 0, green: 0, blue: 0}, + visible: false, + text: "Me" + }); + } + + if (myAvatarIcon) { + Overlays.editOverlay(myAvatarIcon, { + visible: true, + dimensions: iconDimensions, + position: iconPos + }); + + } + var textSize = (14 + (iconDimensions.y - 0.03) * 15 / 0.06); + + Overlays.editOverlay(myAvatarName, { + visible: true, + x: x - 18 + (iconDimensions.y - 0.03) * 2 / 0.06, + y: y + iconDimensions.y * 550, + font: {size: textSize, bold: true}, + }); + + +} + +function hideAllAvatarIcons() { + var len = avatarsIcons.length; + for (var i = 0; i < len; i++) { + Overlays.editOverlay(avatarsIcons[i], {visible: false}); + } + len = avatarsNames.length; + for (var j = 0; j < len; j++) { + Overlays.editOverlay(avatarsNames[j], {visible: false}); + } + if (myAvatarIcon) { + Overlays.editOverlay(myAvatarIcon, {visible: false}); + } + Overlays.editOverlay(myAvatarName, {visible: false}) +} + +function renderAllOthersAvatarIcons() { + var avatarPos; + var iconDimensions = avatarIconPlaneDimensions(); + var commonY = Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS; + var borderPoints = [ + computePointAtPlaneY(0, 0, commonY), + computePointAtPlaneY(Window.innerWidth, Window.innerHeight, commonY) + ]; + + for (var QUuid in avatarsData) { + if (avatarsData.hasOwnProperty(QUuid)) { + if (AvatarList.getAvatar(QUuid) != null) { + avatarPos = AvatarList.getAvatar(QUuid).position; + + var cameraPos = Camera.position; + var p1 = findLineToHeightIntersectionCoords(avatarPos.x, avatarPos.y, avatarPos.z, + cameraPos.x, cameraPos.y, cameraPos.z, + commonY); + + var x = (p1.x - borderPoints[0].x) * (Window.innerWidth) / (borderPoints[1].x - borderPoints[0].x); + var y = (p1.z - borderPoints[0].z) * (Window.innerHeight) / (borderPoints[1].z - borderPoints[0].z); + + if (avatarsData[QUuid].icon != undefined) { + var iconPos = findLineToHeightIntersectionCoords( avatarPos.x, avatarPos.y + RADAR_ICONS_APPARENT_DISTANCE_TO_AVATAR_BASE, avatarPos.z, + Camera.position.x, Camera.position.y, Camera.position.z, + Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS); + if (!iconPos) { print ("avatar icon pos bad for " + QUuid); continue; } + if (avatarsData[QUuid].needRefresh) { + var avat = AvatarList.getAvatar(QUuid); + if (avat && avat.displayName) { + Overlays.editOverlay(avatarsData[QUuid].name, { + width: avat.displayName.length * AVATAR_DISPLAY_NAME_CHAR_WIDTH, + text: avat.displayName, + textAlignCenter: true + }); + avatarsData[QUuid].needRefresh = false; + } + } + var textSize = (14 + (iconDimensions.y - 0.03) * 15 / 0.06); + Overlays.editOverlay(avatarsData[QUuid].icon, { + visible: true, + dimensions: iconDimensions, + position: iconPos + }); + Overlays.editOverlay(avatarsData[QUuid].name, { + visible: true, + x: x - avatarsData[QUuid].textWidth * 0.5, + y: y + iconDimensions.y * 550, + font: {size: textSize, bold: true} + }); + } + } + } + } +} + +function entityAdded(entityID) { + printd ("Entity added " + entityID); + var props = Entities.getEntityProperties(entityID, ["type"]); + printd ("Entity added " + entityID + " PROPS " + JSON.stringify(props)); + if (props && props.type == "Web") { + printd ("Entity Web added " + entityID); + saveEntityData(entityID, true); + } +} + +function entityRemoved(entityID) { + printd ("Entity removed " + entityID); + var props = Entities.getEntityProperties(entityID, ["type"]); + if (props && props.type == "Web") { + print ("Entity Web removed " + entityID); + removeEntityData(entityID); + } +} + +/******************************************************************************************************** + * Entities (to remark) cache structure for showing entities markers + ********************************************************************************************************/ + +var entitiesData = {}; // by entityID +var entitiesByOverlayID = {}; // by overlayID +var entitiesIcons = []; // a parallel list of icons (overlays) to easily run through + +var ICON_ENTITY_WEB_MODEL_URL = Script.resolvePath("../assets/images/web.svg"); +var ICON_ENTITY_IMG_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); // FIXME - use correct model&texture +var ICON_ENTITY_DEFAULT_DIMENSIONS = { + x: 0.10, + y: 0.00001, + z: 0.10 +}; + +var entityIconModelDimensionsVal = { x: 0, y: 0.00001, z: 0}; +function entityIconModelDimensions() { + // given the current height, give a size + var xz = -0.002831 * radarHeight + 0.1; + entityIconModelDimensionsVal.x = xz; + entityIconModelDimensionsVal.z = xz; + // reuse object + return entityIconModelDimensionsVal; +} +/* + * entityIconPlaneDimensions: similar to entityIconModelDimensions but using xy plane + */ +function entityIconPlaneDimensions() { + var dim = entityIconModelDimensions(); + var z = dim.z; + dim.z = dim.y; + dim.y = z; + return dim; +} + +function currentOverlayForEntity(QUuid) { + if (entitiesData[QUuid] != undefined) { + return entitiesData[QUuid].icon; + } else { + return null; + } +} + +function saveEntityData(QUuid, planar) { + if (QUuid == null) return; + var entity = Entities.getEntityProperties(QUuid, ["position"]); + printd("entity added save entity " + QUuid); + if (entitiesData[QUuid] != undefined) { + entitiesData[QUuid].position = entity.position; + } else { + var entityIcon = Overlays.addOverlay("image3d", { + subImage: { x: 0, y: 0, width: 150, height: 150}, + url: ICON_ENTITY_WEB_MODEL_URL, + dimensions: ICON_ENTITY_DEFAULT_DIMENSIONS, + visible: false, + ignoreRayIntersection: false, + orientation: Quat.fromPitchYawRollDegrees(-90,0,0) + }); + + + entitiesIcons.push(entityIcon); + entitiesData[QUuid] = { position: entity.position, icon: entityIcon}; + entitiesByOverlayID[entityIcon] = QUuid; + } +} + +function removeEntityData(QUuid) { + if (QUuid == null) return; + + var itsOverlay = currentOverlayForEntity(QUuid); + if (itsOverlay != null) { + Overlays.deleteOverlay(itsOverlay); + delete entitiesByOverlayID[itsOverlay]; + } + var idx = entitiesIcons.indexOf(itsOverlay); + entitiesIcons.splice(idx, 1); + + delete entitiesData[QUuid]; +} + +/******************************************************************************************************** + * Entities to remark Icon/Markers rendering + ********************************************************************************************************/ + +function hideAllEntitiesIcons() { + var len = entitiesIcons.length; + for (var i = 0; i < len; i++) { + Overlays.editOverlay(entitiesIcons[i], {visible: false}); + } +} + +function renderAllEntitiesIcons() { + var entityPos; + var entityProps; + var iconDimensions = entityIconModelDimensions(); + var planeDimensions = entityIconPlaneDimensions(); // plane overlays uses xy instead of xz + for (var QUuid in entitiesData) { + if (entitiesData.hasOwnProperty(QUuid)) { + entityProps = Entities.getEntityProperties(QUuid, ["position","visible"]); + if (entityProps != null) { + entityPos = entityProps.position; + if (entitiesData[QUuid].icon != undefined && entityPos) { + var iconPos = findLineToHeightIntersectionCoords( entityPos.x, entityPos.y + RADAR_ICONS_APPARENT_DISTANCE_TO_AVATAR_BASE, entityPos.z, + Camera.position.x, Camera.position.y, Camera.position.z, + Camera.position.y - RADAR_CAMERA_DISTANCE_TO_ICONS); + if (!iconPos) { printd ("entity icon pos bad for " + QUuid); continue; } + var dimensions = entitiesData[QUuid].planar? planeDimensions : iconDimensions; + Overlays.editOverlay(entitiesData[QUuid].icon, { + visible: entityProps.visible, + dimensions: dimensions, + position: iconPos + }); + } + } + } + } +} + +/******************************************************************************************************** + * + ********************************************************************************************************/ + +function startRadar() { + printd("avatar added my avatar is " + MyAvatar.sessionUUID); + saveAllOthersAvatarsData(); + Camera.mode = "independent"; + + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: radarHeight, z: 0}); + Camera.orientation = Quat.fromPitchYawRollDegrees(-90,0,0); + radar = true; + + connectRadarModeEvents(); +} + +function endRadar() { + printd("-- endRadar"); + Camera.mode = "first person"; + radar = false; + + disconnectRadarModeEvents(); + hideAllEntitiesIcons(); + hideAllAvatarIcons(); +} + +function onRadarModeClicked() { + startRadar(); +} + +function onMyViewModeClicked() { + endRadar(); +} + +radarModeInterface.startRadarMode = function () { + startRadar(); +}; + +radarModeInterface.endRadarMode = function () { + endRadar(); +}; + +radarModeInterface.init = function() { + init(); +} + +radarModeInterface.setUniqueColor = function(c) { + uniqueColor = c; +}; + +module.exports = radarModeInterface; + +function updateRadar() { + // Update avatar icons + if (startedDraggingCamera) { + hideAllAvatarIcons(); + hideAllEntitiesIcons(); + startedDraggingCamera = false; + } else if (!draggingCamera) { + renderMyAvatarIcon(); + renderAllOthersAvatarIcons(); + renderAllEntitiesIcons(); + } +} + +function valueIfDefined(value) { + return value !== undefined ? value : ""; +} + +function entitiesAnalysis() { + var ids = Entities.findEntitiesInFrustum(Camera.frustum); + var entities = []; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var properties = Entities.getEntityProperties(id); + entities.push({ + id: id, + name: properties.name, + type: properties.type, + url: properties.type == "Model" ? properties.modelURL : "", + sourceUrl: properties.sourceUrl, + locked: properties.locked, + visible: properties.visible, + drawCalls: valueIfDefined(properties.renderInfo.drawCalls), + hasScript: properties.script !== "" + }); + } +} + +function connectRadarModeEvents() { + Script.update.connect(updateRadar); // 60Hz loop + Controller.keyPressEvent.connect(keyPressEvent); + Controller.mousePressEvent.connect(mousePress); // single click/touch + Controller.touchUpdateEvent.connect(touchUpdate); + MyAvatar.positionGoneTo.connect(positionGoneTo); +} + +function positionGoneTo() { + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: radarHeight, z: 0}); +} + +function disconnectRadarModeEvents() { + Script.update.disconnect(updateRadar); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.mousePressEvent.disconnect(mousePress); + Controller.touchUpdateEvent.disconnect(touchUpdate); + MyAvatar.positionGoneTo.disconnect(positionGoneTo); +} + +function init() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + + Controller.touchBeginEvent.connect(touchBegin); + Controller.touchEndEvent.connect(touchEnd); + + AvatarList.avatarAddedEvent.connect(avatarAdded); + AvatarList.avatarRemovedEvent.connect(avatarRemoved); + + Entities.addingEntity.connect(entityAdded); + Entities.deletingEntity.connect(entityRemoved); +} + + diff --git a/scripts/system/+android/uniqueColor.js b/scripts/system/+android/uniqueColor.js new file mode 100644 index 0000000000..c296b6c87d --- /dev/null +++ b/scripts/system/+android/uniqueColor.js @@ -0,0 +1,46 @@ +"use strict"; +// +// uniqueColor.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on 17 Oct 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 +// + +var colorsMap = {}; +var colorsCount = 0; + // 'red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'magenta' +var baseColors = [ '#EB3345', '#F0851F', '#FFCD29', '#94C338', '#11A6C5', '#294C9F', '#C01D84' ]; + +function getNextColor(n) { + var N = baseColors.length; + /*if (n < baseColors.length) { + return baseColors[n]; + } else { + var baseColor = baseColors[n % N]; + var d = (n / N) % 10; + var c2 = "" + Qt.lighter(baseColor, 1 + d / 10); + return c2; + }*/ + return baseColors[n%N]; +} + +function getColorForId(uuid) { + if (colorsMap == undefined) { + colorsMap = {}; + } + if (!colorsMap.hasOwnProperty(uuid)) { + colorsMap[uuid] = getNextColor(colorsCount); + colorsCount = colorsCount + 1; + } + return colorsMap[uuid]; +} + +module.exports = { + getColor: function(id) { + return getColorForId(id); + } +};