diff --git a/interface/resources/icons/+android/avatar-a.svg b/interface/resources/icons/+android/avatar-a.svg new file mode 100755 index 0000000000..165b39943e --- /dev/null +++ b/interface/resources/icons/+android/avatar-a.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/interface/resources/icons/+android/avatar-i.svg b/interface/resources/icons/+android/avatar-i.svg new file mode 100755 index 0000000000..c1557487ea --- /dev/null +++ b/interface/resources/icons/+android/avatar-i.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/interface/resources/icons/+android/button-a.svg b/interface/resources/icons/+android/button-a.svg new file mode 100644 index 0000000000..d469154775 --- /dev/null +++ b/interface/resources/icons/+android/button-a.svg @@ -0,0 +1,949 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/button.svg b/interface/resources/icons/+android/button.svg new file mode 100644 index 0000000000..8c19332064 --- /dev/null +++ b/interface/resources/icons/+android/button.svg @@ -0,0 +1,949 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/+android/tick.svg b/interface/resources/icons/+android/tick.svg new file mode 100644 index 0000000000..2c451c0994 --- /dev/null +++ b/interface/resources/icons/+android/tick.svg @@ -0,0 +1,950 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/controls-uit/+android/ImageButton.qml b/interface/resources/qml/controls-uit/+android/ImageButton.qml new file mode 100644 index 0000000000..5ebf7cd3e9 --- /dev/null +++ b/interface/resources/qml/controls-uit/+android/ImageButton.qml @@ -0,0 +1,82 @@ +// +// ImageButton.qml +// interface/resources/qml/controls-uit +// +// Created by Gabriel Calero & Cristian Duarte on 12 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.Layouts 1.3 +import "../styles-uit" as HifiStyles + +Item { + id: button + + property string text: "" + property string source : "" + property string hoverSource : "" + property real fontSize: 10 + property string fontColor: "#FFFFFF" + property string hoverFontColor: "#000000" + + signal clicked(); + + Rectangle { + color: "transparent" + anchors.fill: parent + Image { + id: image + anchors.fill: parent + source: button.source + } + + HifiStyles.FiraSansRegular { + id: buttonText + anchors.centerIn: parent + text: button.text + color: button.fontColor + font.pixelSize: button.fontSize + } + + MouseArea { + anchors.fill: parent + onClicked: button.clicked(); + onEntered: { + button.state = "hover state"; + } + onExited: { + button.state = "base state"; + } + } + + + } + states: [ + State { + name: "hover state" + PropertyChanges { + target: image + source: button.hoverSource + } + PropertyChanges { + target: buttonText + color: button.hoverFontColor + } + }, + State { + name: "base state" + PropertyChanges { + target: image + source: button.source + } + PropertyChanges { + target: buttonText + color: button.fontColor + } + } + ] +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/+android/AvatarOption.qml b/interface/resources/qml/hifi/+android/AvatarOption.qml new file mode 100644 index 0000000000..e7056baa36 --- /dev/null +++ b/interface/resources/qml/hifi/+android/AvatarOption.qml @@ -0,0 +1,117 @@ +// +// AvatarOption.qml +// interface/resources/qml/hifi/android +// +// Created by Cristian Duarte & Gabriel Calero on 12 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.Layouts 1.3 +import QtQuick 2.5 +import "../controls-uit" as HifiControlsUit + +ColumnLayout { + id: itemRoot + + property string type: ""; + + property string thumbnailUrl: ""; + property string avatarUrl: ""; + property string avatarName: ""; + property bool avatarSelected: false; + + property string methodName: ""; + property string actionText: ""; + + spacing: 4*3 + signal sendToParentQml(var message); + + Image { + id: itemImage + Layout.preferredWidth: 250*3 + Layout.preferredHeight: 140*3 + source: thumbnailUrl + asynchronous: true + fillMode: Image.PreserveAspectFit + + MouseArea { + id: itemArea + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + if (type=="avatar") { + if (!avatarSelected) sendToParentQml({ method: "selectAvatar", params: { avatarUrl: avatarUrl } }); + } else { + sendToParentQml({ method: methodName, params: { } }); + } + } + } + + } + + Text { + id: itemName + text: avatarName + color: "#FFFFFF" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.horizontalCenter: itemImage.horizontalCenter + font.pointSize: 5*3 + wrapMode: Text.WordWrap + width: parent + MouseArea { + id: itemNameArea + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + if (type=="avatar") { + if (!avatarSelected) sendToParentQml({ method: "selectAvatar", params: { avatarUrl: avatarUrl } }); + } else { + sendToParentQml({ method: methodName, params: { } }); + } + } + } + } + + HifiControlsUit.ImageButton { + width: 140*3 + height: 35*3 + text: type=="extra"? actionText: "CHOOSE" + source: "../../../../icons/button.svg" + hoverSource: "../../../../icons/button-a.svg" + fontSize: 18*3 + fontColor: "#2CD8FF" + hoverFontColor: "#FFFFFF" + anchors { + horizontalCenter: itemName.horizontalCenter + } + visible: !avatarSelected + onClicked: { + if (type=="avatar") { + if (!avatarSelected) sendToParentQml({ method: "selectAvatar", params: { avatarUrl: avatarUrl } }); + } else { + sendToParentQml({ method: methodName, params: { } }); + } + } + } + + Image { + id: tickImage + width: 35*3 + height: 35*3 + source: "../../../icons/tick.svg" + anchors { + horizontalCenter: itemName.horizontalCenter + } + visible: avatarSelected + } + + Component.onCompleted:{ + sendToParentQml.connect(sendToScript); + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/+android/HifiConstants.qml b/interface/resources/qml/hifi/+android/HifiConstants.qml index ee6d92ed38..f74cc554aa 100644 --- a/interface/resources/qml/hifi/+android/HifiConstants.qml +++ b/interface/resources/qml/hifi/+android/HifiConstants.qml @@ -20,8 +20,8 @@ Item { Item { id: dimen - readonly property real windowLessWidth: 126 - readonly property real windowLessHeight: 64 + readonly property real windowLessWidth: 126*3 + readonly property real windowLessHeight: 64*3 readonly property real windowZ: 100 diff --git a/interface/resources/qml/hifi/+android/avatarSelection.qml b/interface/resources/qml/hifi/+android/avatarSelection.qml new file mode 100644 index 0000000000..3090204308 --- /dev/null +++ b/interface/resources/qml/hifi/+android/avatarSelection.qml @@ -0,0 +1,175 @@ +// +// avatarSelection.qml +// interface/resources/qml/android +// +// Created by Gabriel Calero & Cristian Duarte on 21 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 +// +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import Hifi 1.0 + +import "../../styles" +import "." +import ".." +import ".." as QmlHifi +import "../../styles-uit" as HifiStyles + + +Item { + + id: top + + 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 } + + signal sendToScript(var message); + + property bool shown: true + + onShownChanged: { + top.visible = shown; + } + + + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifiStyleConstants } + + property int cardWidth: 250 *3; + property int cardHeight: 240 *3; + property int gap: 14 *3; + + property var avatarsArray: []; + property var extraOptionsArray: []; + + function hide() { + shown = false; + sendToScript ({ method: "hide" }); + } + + Rectangle { + + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + + gradient: Gradient { + GradientStop { position: 0.0; color: android.color.gradientTop } + GradientStop { position: 1.0; color: android.color.gradientBottom } + } + + QmlHifi.WindowHeader { + id: header + iconSource: "../../../../icons/avatar-i.svg" + titleText: "AVATAR" + } + + ListModel { id: avatars } + + ListView { + id: scroll + height: 250*3 + property int stackedCardShadowHeight: 10*3; + spacing: gap; + clip: true; + anchors { + left: parent.left + right: parent.right + top: header.bottom + topMargin: gap + leftMargin: gap + rightMargin: gap + } + model: avatars; + orientation: ListView.Horizontal; + delegate: QmlHifi.AvatarOption { + type: model.type; + thumbnailUrl: model.thumbnailUrl; + avatarUrl: model.avatarUrl; + avatarName: model.avatarName; + avatarSelected: model.avatarSelected; + methodName: model.methodName; + actionText: model.actionText; + } + highlightMoveDuration: -1; + highlightMoveVelocity: -1; + } + + } + + function escapeRegExp(str) { + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); + } + function replaceAll(str, find, replace) { + return str.replace(new RegExp(escapeRegExp(find), 'g'), replace); + } + + function refreshSelected(selectedAvatarUrl) { + // URL as ID? + avatarsArray.forEach(function (avatarData) { + avatarData.avatarSelected = (selectedAvatarUrl == avatarData.avatarUrl); + console.log('[avatarSelection] avatar : ', avatarData.avatarName, ' is selected? ' , avatarData.avatarSelected); + }); + } + + function addAvatar(name, thumbnailUrl, avatarUrl) { + avatarsArray.push({ + type: "avatar", + thumbnailUrl: thumbnailUrl, + avatarUrl: avatarUrl, + avatarName: name, + avatarSelected: false, + methodName: "", + actionText: "" + }); + } + + function showAvatars() { + avatars.clear(); + avatarsArray.forEach(function (avatarData) { + avatars.append(avatarData); + console.log('[avatarSelection] adding avatar to model: ', JSON.stringify(avatarData)); + }); + extraOptionsArray.forEach(function (extraData) { + avatars.append(extraData); + console.log('[avatarSelection] adding extra option to model: ', JSON.stringify(extraData)); + }); + } + + function addExtraOption(showName, thumbnailUrl, methodNameWhenClicked, actionText) { + extraOptionsArray.push({ + type: "extra", + thumbnailUrl: thumbnailUrl, + avatarUrl: "", + avatarName: showName, + avatarSelected: false, + methodName: methodNameWhenClicked, + actionText: actionText + }); + } + + function fromScript(message) { + //console.log("[CHAT] fromScript " + JSON.stringify(message)); + switch (message.type) { + case "addAvatar": + addAvatar(message.name, message.thumbnailUrl, message.avatarUrl); + break; + case "addExtraOption": + //(showName, thumbnailUrl, methodNameWhenClicked, actionText) + addExtraOption(message.showName, message.thumbnailUrl, message.methodNameWhenClicked, message.actionText); + break; + case "refreshSelected": + refreshSelected(message.selectedAvatarUrl); + break; + case "showAvatars": + showAvatars(); + break; + default: + } + } +} \ No newline at end of file diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6fec6f1d72..cbcd08c667 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5939,6 +5939,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("App", this); + qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); DependencyManager::get()->registerMetaTypes(scriptEngine.data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 8f0690bda1..5161c62a65 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -389,6 +389,8 @@ public slots: const QString getPreferredCursor() const { return _preferredCursor.get(); } void setPreferredCursor(const QString& cursor); + Q_INVOKABLE bool askBeforeSetAvatarUrl(const QString& avatarUrl) { return askToSetAvatarUrl(avatarUrl); } + private slots: void showDesktop(); void clearDomainOctreeDetails(); diff --git a/scripts/system/+android/avatarSelection.js b/scripts/system/+android/avatarSelection.js new file mode 100644 index 0000000000..fd938236fb --- /dev/null +++ b/scripts/system/+android/avatarSelection.js @@ -0,0 +1,159 @@ +"use strict"; +// +// avatarSelection.js +// scripts/system/ +// +// Created by Gabriel Calero & Cristian Duarte on 21 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 = true; +var isVisible = false; + +function printd(str) { + if (logEnabled) + print("[avatarSelection.js] " + str); +} + +function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + var data; + printd("fromQml " + JSON.stringify(message)); + switch (message.method) { + case 'selectAvatar': + // use this message.params.avatarUrl + printd("Selected Avatar: [" + message.params.avatarUrl + "]"); + App.askBeforeSetAvatarUrl(message.params.avatarUrl); + break; + case 'openAvatarMarket': + // good + App.openUrl("https://metaverse.highfidelity.com/marketplace?category=avatars"); + break; + case 'hide': + module.exports.onHidden(); + break; + default: + print('[avatarSelection.js] Unrecognized message from avatarSelection.qml:', JSON.stringify(message)); + } +} + +function sendToQml(message) { + if (!window) { + print("[avatarSelection.js] There is no window object"); + return; + } + window.sendToQml(message); +} + +function refreshSelected(currentAvatarURL) { + sendToQml({ + type: "refreshSelected", + selectedAvatarUrl: currentAvatarURL + }); + + sendToQml({ + type: "showAvatars" + }); +} + +function init() { + if (!window) { + print("[avatarSelection.js] There is no window object for init()"); + return; + } + var DEFAULT_AVATAR_URL = "http://mpassets.highfidelity.com/f14bf7c9-49a1-4249-988a-0a577ed78957-v1/beingOfLight.fst"; + sendToQml({ + type: "addAvatar", + name: "Being of Light Avatar", + thumbnailUrl: "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/f14bf7c9-49a1-4249-988a-0a577ed78957/thumbnail/hifi-mp-f14bf7c9-49a1-4249-988a-0a577ed78957.jpg", + avatarUrl: DEFAULT_AVATAR_URL + }); + sendToQml({ + type: "addAvatar", + name: "Cody", + thumbnailUrl: "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/8c859fca-4cbd-4e82-aad1-5f4cb0ca5d53/thumbnail/hifi-mp-8c859fca-4cbd-4e82-aad1-5f4cb0ca5d53.jpg", + avatarUrl: "http://mpassets.highfidelity.com/8c859fca-4cbd-4e82-aad1-5f4cb0ca5d53-v1/cody.fst" + }); + sendToQml({ + type: "addAvatar", + name: "Mixamo Will", + thumbnailUrl: "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/d029ae8d-2905-4eb7-ba46-4bd1b8cb9d73/thumbnail/hifi-mp-d029ae8d-2905-4eb7-ba46-4bd1b8cb9d73.jpg", + avatarUrl: "http://mpassets.highfidelity.com/d029ae8d-2905-4eb7-ba46-4bd1b8cb9d73-v1/4618d52e711fbb34df442b414da767bb.fst" + }); + sendToQml({ + type: "addAvatar", + name: "Albert", + thumbnailUrl: "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/1e57c395-612e-4acd-9561-e79dbda0bc49/thumbnail/hifi-mp-1e57c395-612e-4acd-9561-e79dbda0bc49.jpg", + avatarUrl: "http://mpassets.highfidelity.com/1e57c395-612e-4acd-9561-e79dbda0bc49-v1/albert.fst" + }); + /* We need to implement the wallet, so let's skip this for the moment + sendToQml({ + type: "addExtraOption", + showName: "More choices", + thumbnailUrl: "../../../images/moreAvatars.png", + methodNameWhenClicked: "openAvatarMarket", + actionText: "MARKETPLACE" + }); + */ + var currentAvatarURL = Settings.getValue('Avatar/fullAvatarURL', DEFAULT_AVATAR_URL); + printd("Default Avatar: [" + DEFAULT_AVATAR_URL + "]"); + printd("Current Avatar: [" + currentAvatarURL + "]"); + if (!currentAvatarURL || 0 === currentAvatarURL.length) { + currentAvatarURL = DEFAULT_AVATAR_URL; + } + refreshSelected(currentAvatarURL); +} + +module.exports = { + init: function() { + window = new QmlFragment({ + qml: "hifi/avatarSelection.qml", + visible: false + }); + /*, + visible: false*/ + if (window) { + window.fromQml.connect(fromQml); + } + init(); + }, + show: function() { + if (window) { + window.setVisible(true); + isVisible = true; + } + }, + hide: function() { + if (window) { + window.setVisible(false); + } + isVisible = false; + }, + destroy: function() { + if (window) { + window.fromQml.disconnect(fromQml); + 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; + }, + refreshSelectedAvatar: function(currentAvatarURL) { + refreshSelected(currentAvatarURL); + }, + onHidden: function() { } +}; diff --git a/scripts/system/+android/bottombar.js b/scripts/system/+android/bottombar.js index e58840ad6f..db05b88b04 100644 --- a/scripts/system/+android/bottombar.js +++ b/scripts/system/+android/bottombar.js @@ -14,8 +14,10 @@ var bottombar; var bottomHudOptionsBar; var gotoBtn; +var avatarBtn; var gotoScript = Script.require('./goto.js'); +var avatarSelection = Script.require('./avatarSelection.js'); var logEnabled = false; @@ -34,6 +36,8 @@ function init() { hideAddressBar(); } }); + avatarSelection.init(); + App.fullAvatarURLChanged.connect(processedNewAvatar); setupBottomBar(); setupBottomHudOptionsBar(); @@ -43,6 +47,7 @@ function init() { } function shutdown() { + App.fullAvatarURLChanged.disconnect(processedNewAvatar); } function setupBottomBar() { @@ -60,6 +65,32 @@ function setupBottomBar() { } }); + avatarBtn = bottombar.addButton({ + icon: "icons/avatar-i.svg", + activeIcon: "icons/avatar-a.svg", + bgOpacity: 0, + height: 240, + width: 294, + hoverBgOpacity: 0, + activeBgOpacity: 0, + activeHoverBgOpacity: 0, + iconSize: 108, + textSize: 45, + text: "AVATAR" + }); + avatarBtn.clicked.connect(function() { + printd("Avatar button clicked"); + if (!avatarSelection.isVisible()) { + showAvatarSelection(); + } else { + hideAvatarSelection(); + } + }); + avatarSelection.onHidden = function() { + if (avatarBtn) { + avatarBtn.isActive = false; + } + }; gotoBtn = bottombar.addButton({ icon: "icons/goto-i.svg", @@ -69,7 +100,7 @@ function setupBottomBar() { activeBgOpacity: 0, activeHoverBgOpacity: 0, height: 240, - width: 300, + width: 294, iconSize: 108, textSize: 45, text: "GO TO" @@ -148,7 +179,21 @@ function hideAddressBar() { gotoBtn.isActive = false; } +function showAvatarSelection() { + avatarSelection.show(); + avatarBtn.isActive = true; +} +function hideAvatarSelection() { + avatarSelection.hide(); + avatarBtn.isActive = false; +} + +// TODO: Move to avatarSelection.js and make it possible to hide the window from there AND switch the button state here too +function processedNewAvatar(url, modelName) { + avatarSelection.refreshSelectedAvatar(url); + hideAvatarSelection(); +} Script.scriptEnding.connect(function () { shutdown();