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();