wip on avatarapp

This commit is contained in:
Alexander Ivash 2018-04-19 11:59:26 +03:00
parent 10bce7ea8d
commit 1ddfc87396
43 changed files with 2981 additions and 9 deletions

View file

@ -0,0 +1,9 @@
<svg width="24" height="22" viewBox="0 0 24 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Canvas" fill="none">
<g id="Vector">
<path d="M 4.29197 19.3019C 4.20267 19.3019 4.11337 19.2697 4.02407 19.2052C 3.96826 19.1622 3.81199 19.0548 3.90129 18.6894L 4.21383 17.4427C 4.55987 16.0778 4.89474 14.7236 5.24078 13.3588C 5.43054 12.6064 5.20729 11.9509 4.60452 11.4672C 3.80082 10.8117 2.9748 10.1453 2.18227 9.51126C 1.54601 8.99539 0.898591 8.46878 0.262332 7.95292C -0.0278912 7.72723 -0.0167287 7.57677 0.0167586 7.41556C 0.0390835 7.32958 0.0948956 7.07165 0.53023 7.05015C 2.65109 6.91044 4.51522 6.78147 6.23423 6.66326C 7.02677 6.60952 7.6407 6.19038 7.93092 5.48106C 8.34393 4.51382 8.75694 3.54657 9.15879 2.56858C 9.47134 1.83777 9.78389 1.09621 10.0964 0.365404C 10.2192 0.0752303 10.3643 0.0107472 10.5987 1.02493e-08L 10.6099 1.02493e-08C 10.8108 1.02493e-08 10.9671 0.193449 11.0452 0.365404L 11.8266 2.20317C 12.2954 3.29939 12.7643 4.3956 13.2219 5.50256C 13.5121 6.17963 14.1038 6.59877 14.8628 6.65251C 15.7558 6.71699 16.6488 6.78147 17.5195 6.83521C 18.0664 6.86745 18.6245 6.91044 19.1715 6.94268C 19.2943 6.95343 19.4171 6.96418 19.5399 6.96418C 19.9752 6.99642 20.377 7.01791 20.79 7.07165C 21.1249 7.11464 21.1584 7.46929 21.1584 7.57677L 21.1584 7.60901C 21.1584 7.71648 20.9686 7.87769 20.8905 7.94217C 20.4217 8.31832 19.9417 8.68373 19.4729 9.05988C 18.5129 9.81218 17.5306 10.586 16.5595 11.3383C 15.8563 11.8864 15.5995 12.6064 15.8116 13.4447C 16.1242 14.6377 16.4255 15.8521 16.7269 17.0343C 16.8609 17.5716 16.9948 18.109 17.1288 18.6464C 17.1734 18.8398 17.1511 19.0225 17.0506 19.1515C 16.9725 19.2375 16.872 19.2912 16.7381 19.2912C 16.7158 19.2912 16.6934 19.2912 16.6711 19.2912C 16.6488 19.2912 16.5818 19.2697 16.4702 19.2052C 14.7958 18.2272 13.1438 17.2492 11.5587 16.3035C 11.2238 16.0993 10.8666 16.0026 10.5094 16.0026C 10.1522 16.0026 9.79505 16.11 9.44901 16.3142C 8.24347 17.0343 7.02677 17.7651 5.84355 18.4744L 4.61568 19.2052C 4.49289 19.2697 4.38127 19.3019 4.29197 19.3019Z" transform="translate(1.5 1)" fill="#FFB017"/>
<path d="M 4.29197 19.3019C 4.20267 19.3019 4.11337 19.2697 4.02407 19.2052C 3.96826 19.1622 3.81199 19.0548 3.90129 18.6894L 4.21383 17.4427C 4.55987 16.0778 4.89474 14.7236 5.24078 13.3588C 5.43054 12.6064 5.20729 11.9509 4.60452 11.4672C 3.80082 10.8117 2.9748 10.1453 2.18227 9.51126C 1.54601 8.99539 0.898591 8.46878 0.262332 7.95292C -0.0278912 7.72723 -0.0167287 7.57677 0.0167586 7.41556C 0.0390835 7.32958 0.0948956 7.07165 0.53023 7.05015C 2.65109 6.91044 4.51522 6.78147 6.23423 6.66326C 7.02677 6.60952 7.6407 6.19038 7.93092 5.48106C 8.34393 4.51382 8.75694 3.54657 9.15879 2.56858C 9.47134 1.83777 9.78389 1.09621 10.0964 0.365404C 10.2192 0.0752303 10.3643 0.0107472 10.5987 1.02493e-08L 10.6099 1.02493e-08C 10.8108 1.02493e-08 10.9671 0.193449 11.0452 0.365404L 11.8266 2.20317C 12.2954 3.29939 12.7643 4.3956 13.2219 5.50256C 13.5121 6.17963 14.1038 6.59877 14.8628 6.65251C 15.7558 6.71699 16.6488 6.78147 17.5195 6.83521C 18.0664 6.86745 18.6245 6.91044 19.1715 6.94268C 19.2943 6.95343 19.4171 6.96418 19.5399 6.96418C 19.9752 6.99642 20.377 7.01791 20.79 7.07165C 21.1249 7.11464 21.1584 7.46929 21.1584 7.57677L 21.1584 7.60901C 21.1584 7.71648 20.9686 7.87769 20.8905 7.94217C 20.4217 8.31832 19.9417 8.68373 19.4729 9.05988C 18.5129 9.81218 17.5306 10.586 16.5595 11.3383C 15.8563 11.8864 15.5995 12.6064 15.8116 13.4447C 16.1242 14.6377 16.4255 15.8521 16.7269 17.0343C 16.8609 17.5716 16.9948 18.109 17.1288 18.6464C 17.1734 18.8398 17.1511 19.0225 17.0506 19.1515C 16.9725 19.2375 16.872 19.2912 16.7381 19.2912C 16.7158 19.2912 16.6934 19.2912 16.6711 19.2912C 16.6488 19.2912 16.5818 19.2697 16.4702 19.2052C 14.7958 18.2272 13.1438 17.2492 11.5587 16.3035C 11.2238 16.0993 10.8666 16.0026 10.5094 16.0026C 10.1522 16.0026 9.79505 16.11 9.44901 16.3142C 8.24347 17.0343 7.02677 17.7651 5.84355 18.4744L 4.61568 19.2052C 4.49289 19.2697 4.38127 19.3019 4.29197 19.3019Z" stroke-width="1.75" transform="translate(1.5 1)" stroke="#121212"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,8 @@
<svg width="24" height="22" viewBox="0 0 24 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Canvas" fill="none">
<g id="Vector">
<path d="M 4.29197 19.3019C 4.20267 19.3019 4.11337 19.2697 4.02407 19.2052C 3.96826 19.1622 3.81199 19.0548 3.90129 18.6894L 4.21383 17.4427C 4.55987 16.0778 4.89474 14.7236 5.24078 13.3588C 5.43054 12.6064 5.20729 11.9509 4.60452 11.4672C 3.80082 10.8117 2.9748 10.1453 2.18227 9.51126C 1.54601 8.99539 0.898591 8.46878 0.262332 7.95292C -0.0278912 7.72723 -0.0167287 7.57677 0.0167586 7.41556C 0.0390835 7.32958 0.0948956 7.07165 0.53023 7.05015C 2.65109 6.91044 4.51522 6.78147 6.23423 6.66326C 7.02677 6.60952 7.6407 6.19038 7.93092 5.48106C 8.34393 4.51382 8.75694 3.54657 9.15879 2.56858C 9.47134 1.83777 9.78389 1.09621 10.0964 0.365404C 10.2192 0.0752303 10.3643 0.0107472 10.5987 1.02493e-08L 10.6099 1.02493e-08C 10.8108 1.02493e-08 10.9671 0.193449 11.0452 0.365404L 11.8266 2.20317C 12.2954 3.29939 12.7643 4.3956 13.2219 5.50256C 13.5121 6.17963 14.1038 6.59877 14.8628 6.65251C 15.7558 6.71699 16.6488 6.78147 17.5195 6.83521C 18.0664 6.86745 18.6245 6.91044 19.1715 6.94268C 19.2943 6.95343 19.4171 6.96418 19.5399 6.96418C 19.9752 6.99642 20.377 7.01791 20.79 7.07165C 21.1249 7.11464 21.1584 7.46929 21.1584 7.57677L 21.1584 7.60901C 21.1584 7.71648 20.9686 7.87769 20.8905 7.94217C 20.4217 8.31832 19.9417 8.68373 19.4729 9.05988C 18.5129 9.81218 17.5306 10.586 16.5595 11.3383C 15.8563 11.8864 15.5995 12.6064 15.8116 13.4447C 16.1242 14.6377 16.4255 15.8521 16.7269 17.0343C 16.8609 17.5716 16.9948 18.109 17.1288 18.6464C 17.1734 18.8398 17.1511 19.0225 17.0506 19.1515C 16.9725 19.2375 16.872 19.2912 16.7381 19.2912C 16.7158 19.2912 16.6934 19.2912 16.6711 19.2912C 16.6488 19.2912 16.5818 19.2697 16.4702 19.2052C 14.7958 18.2272 13.1438 17.2492 11.5587 16.3035C 11.2238 16.0993 10.8666 16.0026 10.5094 16.0026C 10.1522 16.0026 9.79505 16.11 9.44901 16.3142C 8.24347 17.0343 7.02677 17.7651 5.84355 18.4744L 4.61568 19.2052C 4.49289 19.2697 4.38127 19.3019 4.29197 19.3019Z" stroke-width="1.75" transform="translate(1.5 1)" stroke="#121212"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -25,10 +25,14 @@ Original.RadioButton {
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
readonly property int boxSize: 14
readonly property int boxRadius: 3
readonly property int checkSize: 10
readonly property int checkRadius: 2
property int boxSize: defaultBoxSize
property real scaleFactor: boxSize / defaultBoxSize
readonly property int defaultBoxSize: 14
readonly property int boxRadius: 3 * scaleFactor
readonly property int checkSize: 10 * scaleFactor
readonly property int checkRadius: 2 * scaleFactor
readonly property int indicatorRadius: 7 * scaleFactor
onClicked: {
Tablet.playSound(TabletEnums.ButtonClick);
@ -44,7 +48,7 @@ Original.RadioButton {
id: box
width: boxSize
height: boxSize
radius: 7
radius: indicatorRadius
x: radioButton.leftPadding
y: parent.height / 2 - height / 2
gradient: Gradient {
@ -66,7 +70,7 @@ Original.RadioButton {
id: check
width: checkSize
height: checkSize
radius: 7
radius: indicatorRadius
anchors.centerIn: parent
color: "#00B4EF"
border.width: 1

View file

@ -27,6 +27,9 @@ SpinBox {
property string suffix: ""
property string labelInside: ""
property color colorLabelInside: hifi.colors.white
property color backgroundColor: isLightColorScheme
? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray)
: (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow)
property real controlHeight: height + (spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0)
property int decimals: 2;
property real factor: Math.pow(10, decimals)
@ -69,9 +72,7 @@ SpinBox {
y: spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0
background: Rectangle {
color: isLightColorScheme
? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray)
: (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow)
color: backgroundColor
border.color: spinBoxLabelInside.visible ? spinBoxLabelInside.color : hifi.colors.primaryHighlight
border.width: spinBox.activeFocus ? spinBoxLabelInside.visible ? 2 : 1 : 0
}

View file

@ -0,0 +1,759 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQml.Models 2.1
import QtGraphicalEffects 1.0
import "../controls-uit" as HifiControls
import "../styles-uit"
import "avatarapp"
Rectangle {
id: root
width: 480
height: 706
color: style.colors.white
property string selectedAvatarId: ''
onSelectedAvatarIdChanged: {
console.debug('selectedAvatarId: ', selectedAvatarId)
}
property var selectedAvatar: selectedAvatarId !== '' ? allAvatars.findAvatar(selectedAvatarId) : undefined
onSelectedAvatarChanged: {
console.debug('selectedAvatar: ', selectedAvatar ? selectedAvatar.url : selectedAvatar)
}
property string avatarName: selectedAvatar ? selectedAvatar.name : ''
property string avatarUrl: selectedAvatar ? selectedAvatar.url : null
property int avatarWearablesCount: selectedAvatar && selectedAvatar.wearables !== '' ? selectedAvatar.wearables.split('|').length : 0
property bool isAvatarInFavorites: selectedAvatar ? selectedAvatar.favorite : false
property bool isInManageState: false
Component.onCompleted: {
for(var i = 0; i < allAvatars.count; ++i) {
var originalUrl = allAvatars.get(i).url;
if(originalUrl !== '') {
var resolvedUrl = Qt.resolvedUrl(originalUrl);
console.debug('url: ', originalUrl, 'resolved: ', resolvedUrl);
allAvatars.setProperty(i, 'url', resolvedUrl);
}
}
selectedAvatarId = allAvatars.get(1).url
console.debug('wearables: ', selectedAvatar.wearables)
view.setPage(0)
console.debug('view.currentIndex: ', view.currentIndex);
}
AvatarAppStyle {
id: style
}
AvatarAppHeader {
id: header
z: 100
pageTitle: !settings.visible ? "Avatar" : "Avatar Settings"
avatarIconVisible: !settings.visible
settingsButtonVisible: !settings.visible
onSettingsClicked: {
settings.open();
}
}
Settings {
id: settings
onSaveClicked: function() {
close();
}
onCancelClicked: function() {
close();
}
}
Rectangle {
id: mainBlock
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: favoritesBlock.top
TextStyle1 {
anchors.left: parent.left
anchors.leftMargin: 30
anchors.top: parent.top
anchors.topMargin: 34
}
TextStyle1 {
id: displayNameLabel
anchors.left: parent.left
anchors.leftMargin: 30
anchors.top: parent.top
anchors.topMargin: 34
text: 'Display Name'
}
InputTextStyle4 {
anchors.left: displayNameLabel.right
anchors.leftMargin: 30
anchors.verticalCenter: displayNameLabel.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 36
width: 232
text: 'ThisIsDisplayName'
HiFiGlyphs {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
size: 36
text: "\ue00d"
}
}
ShadowImage {
id: avatarImage
width: 134
height: 134
anchors.left: displayNameLabel.left
anchors.top: displayNameLabel.bottom
anchors.topMargin: 31
source: avatarUrl
}
AvatarWearablesIndicator {
anchors.right: avatarImage.right
anchors.bottom: avatarImage.bottom
anchors.rightMargin: -radius
anchors.bottomMargin: 6.08
wearablesCount: avatarWearablesCount
visible: avatarWearablesCount !== 0
}
Row {
id: star
anchors.top: parent.top
anchors.topMargin: 119
anchors.left: avatarImage.right
anchors.leftMargin: 30.5
spacing: 12.3
Image {
width: 21.2
height: 19.3
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
anchors.verticalCenter: parent.verticalCenter
}
TextStyle5 {
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
anchors.verticalCenter: parent.verticalCenter
MouseArea {
enabled: !isAvatarInFavorites
anchors.fill: parent
onClicked: {
console.debug('selectedAvatar.url', selectedAvatar.url)
createFavorite.onSaveClicked = function() {
selectedAvatar.favorite = true;
pageOfAvatars.setProperty(view.currentIndex, 'favorite', selectedAvatar.favorite)
createFavorite.close();
}
createFavorite.open(selectedAvatar);
}
}
}
}
TextStyle3 {
id: avatarNameLabel
text: avatarName
anchors.left: avatarImage.right
anchors.leftMargin: 30
anchors.top: parent.top
anchors.topMargin: 154
}
TextStyle3 {
id: wearablesLabel
anchors.left: avatarImage.right
anchors.leftMargin: 30
anchors.top: avatarNameLabel.bottom
anchors.topMargin: 16
text: 'Wearables'
}
SquareLabel {
anchors.right: parent.right
anchors.rightMargin: 30
anchors.verticalCenter: avatarNameLabel.verticalCenter
glyphText: "."
glyphRotation: 45
MouseArea {
anchors.fill: parent
onClicked: {
popup.titleText = 'Specify Avatar URL'
popup.bodyText = 'If you want to add a custom avatar, you can specify the URL of the avatar file' +
'(“.fst” extension) here. <a href="#">Learn to make a custom avatar by opening this link on your desktop.</a>'
popup.inputText.visible = true;
popup.inputText.placeholderText = 'Enter Avatar Url';
popup.button1text = 'CANCEL';
popup.button2text = 'CONFIRM';
popup.onButton2Clicked = function() {
popup.close();
}
popup.open();
}
}
}
SquareLabel {
anchors.right: parent.right
anchors.rightMargin: 30
anchors.verticalCenter: wearablesLabel.verticalCenter
glyphText: "\ue02e"
visible: avatarWearablesCount !== 0
MouseArea {
anchors.fill: parent
onClicked: {
adjustWearables.open();
}
}
}
TextStyle3 {
anchors.right: parent.right
anchors.rightMargin: 30
anchors.verticalCenter: wearablesLabel.verticalCenter
font.underline: true
text: "Add"
visible: avatarWearablesCount === 0
MouseArea {
anchors.fill: parent
property url getWearablesUrl: '../../images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png'
// debug only
acceptedButtons: Qt.LeftButton | Qt.RightButton
property int debug_newAvatarIndex: 0
onClicked: {
if(mouse.button == Qt.RightButton) {
for(var i = 0; i < 3; ++i)
{
console.debug('adding avatar...');
var avatar = {
'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png',
'name': 'Lexi' + (++debug_newAvatarIndex),
'wearables': '',
'favorite': false
};
allAvatars.append(avatar)
if(pageOfAvatars.hasGetAvatars())
pageOfAvatars.removeGetAvatars();
if(pageOfAvatars.count !== view.itemsPerPage)
pageOfAvatars.append(avatar);
if(pageOfAvatars.count !== view.itemsPerPage)
pageOfAvatars.appendGetAvatars();
}
return;
}
popup.button2text = 'AvatarIsland'
popup.button1text = 'CANCEL'
popup.titleText = 'Get Wearables'
popup.bodyText = 'Buy wearables from <a href="https://fake.link">Marketplace</a>' + '\n' +
'Wear wearable from <a href="https://fake.link">My Purchases</a>' + '\n' +
'You can visit the domain “AvatarIsland”' + '\n' +
'to get wearables'
popup.imageSource = getWearablesUrl;
popup.onButton2Clicked = function() {
popup.close();
gotoAvatarAppPanel.visible = true;
}
popup.open();
}
}
}
}
Rectangle {
id: favoritesBlock
height: 369
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
color: style.colors.lightGrayBackground
TextStyle1 {
id: favoritesLabel
anchors.top: parent.top
anchors.topMargin: 9
anchors.left: parent.left
anchors.leftMargin: 30
text: "Favorites"
}
TextStyle8 {
id: manageLabel
anchors.top: parent.top
anchors.topMargin: 9
anchors.right: parent.right
anchors.rightMargin: 30
text: isInManageState ? "Back" : "Manage"
color: style.colors.blueHighlight
MouseArea {
anchors.fill: parent
onClicked: {
isInManageState = isInManageState ? false : true;
}
}
}
Item {
anchors.left: parent.left
anchors.leftMargin: 30
anchors.right: parent.right
anchors.rightMargin: 30
anchors.top: favoritesLabel.bottom
anchors.topMargin: 9
anchors.bottom: parent.bottom
GridView {
id: view
anchors.fill: parent
interactive: false;
currentIndex: (selectedAvatarId !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarId) : -1
AvatarsModel {
id: allAvatars
function findAvatar(avatarId) {
console.debug('AvatarsModel: find avatar by', avatarId);
for(var i = 0; i < count; ++i) {
if(get(i).url === avatarId) {
console.debug('avatar found by index: ', i)
return get(i);
}
}
return -1;
}
}
property int itemsPerPage: 8
property int totalPages: Math.ceil((allAvatars.count + 1) / itemsPerPage)
onTotalPagesChanged: {
console.debug('total pages: ', totalPages)
}
property int currentPage: 0;
onCurrentPageChanged: {
console.debug('currentPage: ', currentPage)
currentIndex = Qt.binding(function() {
return (selectedAvatarId !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarId) : -1
})
}
property bool hasNext: currentPage < (totalPages - 1)
onHasNextChanged: {
console.debug('hasNext: ', hasNext)
}
property bool hasPrev: currentPage > 0
onHasPrevChanged: {
console.debug('hasPrev: ', hasPrev)
}
function setPage(pageIndex) {
pageOfAvatars.isUpdating = true;
pageOfAvatars.clear();
var start = pageIndex * itemsPerPage;
var end = Math.min(start + itemsPerPage, allAvatars.count);
for(var itemIndex = 0; start < end; ++start, ++itemIndex) {
var avatarItem = allAvatars.get(start)
console.debug('getting ', start, avatarItem)
pageOfAvatars.append(avatarItem);
}
if(pageOfAvatars.count !== itemsPerPage)
pageOfAvatars.appendGetAvatars();
currentPage = pageIndex;
console.debug('switched to the page with', pageOfAvatars.count, 'items')
pageOfAvatars.isUpdating = false;
}
model: ListModel {
id: pageOfAvatars
property bool isUpdating: false;
property var getMoreAvatars: {'url' : '', 'name' : 'Get More Avatars'}
function findAvatar(avatarId) {
console.debug('pageOfAvatars.findAvatar: ', avatarId);
for(var i = 0; i < count; ++i) {
if(get(i).url === avatarId) {
console.debug('avatar found by index: ', i)
return i;
}
}
return -1;
}
function appendGetAvatars() {
append(getMoreAvatars);
}
function hasGetAvatars() {
return count != 0 && get(count - 1).url === ''
}
function removeGetAvatars() {
if(hasGetAvatars()) {
remove(count - 1)
console.debug('removed get avatars...');
}
}
}
flow: GridView.FlowTopToBottom
cellHeight: 92 + 36
cellWidth: 92 + 18
delegate: Item {
id: delegateRoot
height: GridView.view.cellHeight
width: GridView.view.cellWidth
Item {
id: container
width: 92
height: 92
states: [
State {
name: "hovered"
when: favoriteAvatarMouseArea.containsMouse;
PropertyChanges { target: container; y: -5 }
PropertyChanges { target: favoriteAvatarImage; dropShadowRadius: 10 }
PropertyChanges { target: favoriteAvatarImage; dropShadowVerticalOffset: 6 }
}
]
AvatarThumbnail {
id: favoriteAvatarImage
imageUrl: url
wearablesCount: (wearables && wearables !== '') ? wearables.split('|').length : 0
onWearablesCountChanged: {
console.debug('delegate: AvatarThumbnail.wearablesCount: ', wearablesCount)
}
visible: url !== ''
MouseArea {
id: favoriteAvatarMouseArea
anchors.fill: parent
hoverEnabled: true
property url getWearablesUrl: '../../images/samples/hifi-place-77312e4b-6f48-4eb4-87e2-50444d8e56d1.png'
onClicked: {
if(isInManageState) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.titleText = 'Delete Favorite: {AvatarName}'.replace('{AvatarName}', currentItem.name)
popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from My Purchases.'
popup.imageSource = null;
popup.button1text = 'CANCEL'
popup.button2text = 'DELETE'
popup.onButton2Clicked = function() {
popup.close();
pageOfAvatars.isUpdating = true;
console.debug('removing ', index)
var absoluteIndex = view.currentPage * view.itemsPerPage + index
console.debug('removed ', absoluteIndex, 'view.currentPage', view.currentPage,
'view.itemsPerPage: ', view.itemsPerPage, 'index', index, 'pageOfAvatars', pageOfAvatars, 'pageOfAvatars.count', pageOfAvatars)
allAvatars.remove(absoluteIndex)
pageOfAvatars.remove(index);
var itemsOnPage = pageOfAvatars.count;
var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage;
console.debug('newItemIndex: ', newItemIndex, 'allAvatars.count - 1: ', allAvatars.count - 1, 'pageOfAvatars.count:', pageOfAvatars.count);
if(newItemIndex <= (allAvatars.count - 1)) {
pageOfAvatars.append(allAvatars.get(newItemIndex));
} else {
if(!pageOfAvatars.hasGetAvatars())
pageOfAvatars.appendGetAvatars();
}
console.debug('removed ', absoluteIndex, 'newItemIndex: ', newItemIndex, 'allAvatars.count:', allAvatars.count, 'pageOfAvatars.count:', pageOfAvatars.count)
pageOfAvatars.isUpdating = false;
};
popup.open();
} else {
if(delegateRoot.GridView.view.currentIndex !== index) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.button2text = 'CONFIRM'
popup.button1text = 'CANCEL'
popup.titleText = 'Load Favorite: {AvatarName}'.replace('{AvatarName}', currentItem.name)
popup.bodyText = 'This will switch your current avatar and ararables that you are wearing with a new avatar and wearables.'
popup.imageSource = null;
popup.onButton2Clicked = function() {
selectedAvatarId = currentItem.url;
popup.close();
delegateRoot.GridView.view.currentIndex = index;
}
popup.open();
}
}
}
}
}
Rectangle {
id: highlight
anchors.fill: favoriteAvatarImage
visible: delegateRoot.GridView.isCurrentItem
color: 'transparent'
border.width: 2
border.color: style.colors.blueHighlight
}
Colorize {
anchors.fill: favoriteAvatarImage
source: favoriteAvatarImage
saturation: 0.2
visible: isInManageState && !highlight.visible
}
HiFiGlyphs {
anchors.fill: parent
text: "{"
visible: isInManageState && !highlight.visible
horizontalAlignment: Text.AlignHCenter
size: 56
}
ShadowRectangle {
width: 92
height: 92
color: style.colors.blueHighlight
visible: url === ''
HiFiGlyphs {
anchors.centerIn: parent
color: 'white'
size: 60
text: "K"
}
MouseArea {
anchors.fill: parent
property url getAvatarsUrl: '../../images/samples/hifi-place-get-avatars.png'
onClicked: {
console.debug('getAvatarsUrl: ', getAvatarsUrl);
popup.button2text = 'BodyMarkt'
popup.button1text = 'CANCEL'
popup.titleText = 'Get Avatars'
popup.bodyText = 'Buy avatars from <a href="https://fake.link">Marketplace</a>' + '\n' +
'Wear avatars from <a href="https://fake.link">My Purchases</a>' + '\n' +
'You can visit the domain “BodyMart”' + '\n' +
'to get avatars'
popup.imageSource = getAvatarsUrl;
popup.onButton2Clicked = function() {
popup.close();
gotoAvatarAppPanel.visible = true;
}
popup.open();
}
}
}
}
TextStyle7 {
id: text
width: 92
anchors.top: container.bottom
anchors.topMargin: 8
anchors.horizontalCenter: container.horizontalCenter
verticalAlignment: Text.AlignTop
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: name
}
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
HiFiGlyphs {
rotation: 180
text: "\ue01d";
size: 50
color: view.hasPrev ? 'black' : 'gray'
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
enabled: view.hasPrev
onClicked: {
view.setPage(view.currentPage - 1)
}
}
}
spacing: 0
HiFiGlyphs {
text: "\ue01d";
size: 50
color: view.hasNext ? 'black' : 'gray'
horizontalAlignment: Text.AlignHCenter
MouseArea {
anchors.fill: parent
enabled: view.hasNext
onClicked: {
view.setPage(view.currentPage + 1)
}
}
}
anchors.bottom: parent.bottom
anchors.bottomMargin: 19
}
}
AdjustWearables {
id: adjustWearables
z: 2
}
MessageBox {
id: popup
}
CreateFavoriteDialog {
id: createFavorite
}
Rectangle {
id: gotoAvatarAppPanel
anchors.fill: parent
anchors.leftMargin: 19
anchors.rightMargin: 19
// color: 'green'
visible: false
onVisibleChanged: {
if(visible) {
console.debug('selectedAvatar.wearables: ', selectedAvatar.wearables)
selectedAvatar.wearables = 'hat|sunglasses|bracelet'
pageOfAvatars.setProperty(view.currentIndex, 'wearables', selectedAvatar.wearables)
}
}
Rectangle {
width: 442
height: 447
// color: 'yellow'
anchors.bottom: parent.bottom
anchors.bottomMargin: 259
TextStyle1 {
anchors.fill: parent
horizontalAlignment: "AlignHCenter"
wrapMode: "WordWrap"
text: "You are teleported to “AvatarIsland” VR world and you buy a hat, sunglasses and a bracelet."
}
}
Rectangle {
width: 442
height: 177
// color: 'yellow'
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
TextStyle1 {
anchors.fill: parent
horizontalAlignment: "AlignHCenter"
wrapMode: "WordWrap"
text: '<a href="https://fake.link">Click here to open the Avatar app.</a>'
MouseArea {
anchors.fill: parent
property int newAvatarIndex: 0
onClicked: {
gotoAvatarAppPanel.visible = false;
var avatar = {
'url': '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png',
'name': 'Lexi' + (++newAvatarIndex),
'wearables': '',
'favorite': false
};
allAvatars.append(avatar)
if(pageOfAvatars.hasGetAvatars())
pageOfAvatars.removeGetAvatars();
if(pageOfAvatars.count !== view.itemsPerPage)
pageOfAvatars.append(avatar);
if(pageOfAvatars.count !== view.itemsPerPage)
pageOfAvatars.appendGetAvatars();
console.debug('avatar appended: allAvatars.count: ', allAvatars.count, 'pageOfAvatars.count: ', pageOfAvatars.count);
}
}
}
}
}
}

View file

@ -0,0 +1,183 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root;
visible: false;
width: 480
height: 706
color: 'lightgray'
property bool modified: false;
Component.onCompleted: {
modified = false;
}
onModifiedChanged: {
console.debug('modified: ', modified)
}
property var onButton2Clicked;
property var onButton1Clicked;
function open() {
visible = true;
}
function close() {
visible = false;
}
HifiConstants { id: hifi }
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
}
Column {
anchors.top: parent.top
anchors.topMargin: 15
anchors.horizontalCenter: parent.horizontalCenter
spacing: 20
width: parent.width - 30 * 2
TextStyle5 {
anchors.horizontalCenter: parent.horizontalCenter
text: "Adjust Wearables"
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
height: 2
color: 'gray'
}
HifiControlsUit.ComboBox {
anchors.left: parent.left
anchors.right: parent.right
model: [
'Fedora.fbx [HeadTop_End]',
'Fedora1.fbx [HeadTop_End]',
'Fedora2.fbx [HeadTop_End]'
]
}
Column {
width: parent.width
spacing: 5
Row {
spacing: 20
TextStyle5 {
text: "Position"
}
TextStyle7 {
text: "m"
}
}
Vector3 {
id: position
onXvalueChanged: modified = true;
onYvalueChanged: modified = true;
onZvalueChanged: modified = true;
}
}
Column {
width: parent.width
spacing: 5
Row {
spacing: 20
TextStyle5 {
text: "Rotation"
}
TextStyle7 {
text: "deg"
}
}
Vector3 {
id: rotation
onXvalueChanged: modified = true;
onYvalueChanged: modified = true;
onZvalueChanged: modified = true;
}
}
Column {
width: parent.width
spacing: 5
TextStyle5 {
text: "Scale"
}
Item {
width: parent.width
height: childrenRect.height
HifiControlsUit.SpinBox {
id: scalespinner
value: 0
backgroundColor: "darkgray"
width: position.spinboxWidth
colorScheme: hifi.colorSchemes.light
onValueChanged: modified = true;
}
HifiControlsUit.Button {
anchors.right: parent.right
color: hifi.buttons.red;
colorScheme: hifi.colorSchemes.dark;
text: "TAKE IT OFF"
}
}
}
}
DialogButtons {
anchors.bottom: parent.bottom
anchors.bottomMargin: 30
anchors.left: parent.left
anchors.leftMargin: 30
anchors.right: parent.right
anchors.rightMargin: 30
yesText: "SAVE"
noText: "CANCEL"
onYesClicked: function() {
if(onButton2Clicked) {
onButton2Clicked();
} else {
root.close();
}
}
onNoClicked: function() {
if(onButton1Clicked) {
onButton1Clicked();
} else {
root.close();
}
}
}
}

View file

@ -0,0 +1,57 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import "../../styles-uit"
ShadowRectangle {
id: header
anchors.left: parent.left
anchors.right: parent.right
height: 84
property alias pageTitle: title.text
property alias avatarIconVisible: avatarIcon.visible
property alias settingsButtonVisible: settingsButton.visible
signal settingsClicked;
AvatarAppStyle {
id: style
}
color: style.colors.lightGrayBackground
HiFiGlyphs {
id: avatarIcon
anchors.left: parent.left
anchors.leftMargin: 23
anchors.top: parent.top
anchors.topMargin: 29
size: 38
text: "<"
}
TextStyle6 {
id: title
anchors.left: avatarIcon.visible ? avatarIcon.right : avatarIcon.left
anchors.leftMargin: 4
anchors.verticalCenter: avatarIcon.verticalCenter
text: 'Avatar'
}
HiFiGlyphs {
id: settingsButton
anchors.right: parent.right
anchors.rightMargin: 30
anchors.verticalCenter: avatarIcon.verticalCenter
text: "&"
MouseArea {
id: settingsMouseArea
anchors.fill: parent
onClicked: {
settingsClicked();
}
}
}
}

View file

@ -0,0 +1,29 @@
//
// HiFiConstants.qml
//
// Created by Alexander Ivash on 17 Apr 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 QtQuick 2.5
import QtQuick.Window 2.2
import "../../styles-uit"
QtObject {
readonly property QtObject colors: QtObject {
readonly property color lightGrayBackground: "#f2f2f2"
readonly property color black: "#000000"
readonly property color white: "#ffffff"
readonly property color blueHighlight: "#00b4ef"
readonly property color inputFieldBackground: "#d4d4d4"
readonly property color yellowishOrange: "#ffb017"
readonly property color blueAccent: "#0093c5"
readonly property color greenHighlight: "#1fc6a6"
readonly property color lightGray: "#afafaf"
readonly property color redHighlight: "#ea4c5f"
readonly property color orangeAccent: "#ff6309"
}
}

View file

@ -0,0 +1,30 @@
import QtQuick 2.9
Item {
width: 92
height: 92
property int wearablesCount: 0
onWearablesCountChanged: {
console.debug('AvatarThumbnail: wearablesCount = ', wearablesCount)
}
property alias dropShadowRadius: avatarImage.dropShadowRadius
property alias dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset
property alias dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset
property alias imageUrl: avatarImage.source
ShadowImage {
id: avatarImage
anchors.fill: parent
}
AvatarWearablesIndicator {
anchors.left: avatarImage.left
anchors.bottom: avatarImage.bottom
anchors.leftMargin: 57
wearablesCount: parent.wearablesCount
visible: parent.wearablesCount !== 0
}
}

View file

@ -0,0 +1,40 @@
import QtQuick 2.9
import "../../controls-uit"
import "../../styles-uit"
Rectangle {
property int wearablesCount: 0
width: 46.5
height: 46.5
radius: width / 2
AvatarAppStyle {
id: style
}
color: style.colors.greenHighlight
HiFiGlyphs {
width: 26.5
height: 13.8
anchors.top: parent.top
anchors.topMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
text: "\ue02e"
}
Item {
width: 46.57
height: 23
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 2.76
TextStyle2 {
anchors.horizontalCenter: parent.horizontalCenter
text: wearablesCount
}
}
}

View file

@ -0,0 +1,48 @@
import QtQuick 2.9
ListModel {
id: model
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d.png'
name: 'Woody'
wearables: ''
favorite: false
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-1.png'
name: 'Damien'
wearables: ''
favorite: false
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-2.png'
name: 'Lexi'
wearables: ''
favorite: false
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-3.png'
name: 'Judie'
wearables: ''
favorite: true
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-4.png'
name: 'Alex'
wearables: ''
favorite: true
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png'
name: 'Matthew'
wearables: ''
favorite: true
}
ListElement {
url: '../../images/samples/hifi-mp-e76946cc-c272-4adf-9bb6-02cde0a4b57d-5.png'
name: 'Ogre'
wearables: ''
favorite: true
}
}

View file

@ -0,0 +1,13 @@
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
HifiControlsUit.Button {
HifiConstants {
id: hifi
}
color: hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
height: 40
}

View file

@ -0,0 +1,136 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root;
visible: false;
anchors.fill: parent;
color: Qt.rgba(0, 0, 0, 0.5);
z: 999;
property string titleText: 'Create Favorite'
property string favoriteNameText: favoriteName.text
property string avatarImageUrl: null
property int wearablesCount: 0
property string button1color: hifi.buttons.noneBorderlessGray;
property string button1text: 'CANCEL'
property string button2color: hifi.buttons.blue;
property string button2text: 'CONFIRM'
property var onSaveClicked;
property var onCancelClicked;
function open(avatar) {
favoriteName.text = '';
avatarImageUrl = avatar.url;
wearablesCount = avatar.wearables !== '' ? avatar.wearables.split('|').length : 0;
visible = true;
}
function close() {
console.debug('closing');
visible = false;
}
HifiConstants {
id: hifi
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
}
Rectangle {
id: mainContainer;
width: Math.max(parent.width * 0.8, 400)
property int margin: 30;
height: childrenRect.height + margin * 2
onHeightChanged: {
console.debug('mainContainer: height = ', height)
}
anchors.centerIn: parent
color: "white"
TextStyle1 {
id: title
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 30
anchors.leftMargin: 30
anchors.rightMargin: 30
text: root.titleText
}
Item {
id: contentContainer
width: parent.width - 50
height: childrenRect.height
anchors.top: title.bottom
anchors.topMargin: 10
anchors.left: parent.left;
anchors.leftMargin: 30;
anchors.right: parent.right;
anchors.rightMargin: 30;
Row {
id: bodyRow
spacing: 44
AvatarThumbnail {
imageUrl: avatarImageUrl
wearablesCount: avatarWearablesCount
}
InputTextStyle4 {
id: favoriteName
anchors.verticalCenter: parent.verticalCenter
placeholderText: "Enter Favorite Name"
}
}
DialogButtons {
anchors.top: bodyRow.bottom
anchors.topMargin: 20
anchors.left: parent.left
anchors.right: parent.right
yesButton.enabled: favoriteNameText !== ''
yesText: root.button2text
noText: root.button1text
onYesClicked: function() {
if(onSaveClicked) {
onSaveClicked();
} else {
root.close();
}
}
onNoClicked: function() {
if(onCancelClicked) {
onCancelClicked();
} else {
root.close();
}
}
}
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.9
Row {
id: root
property string yesText;
property string noText;
property var onYesClicked;
property var onNoClicked;
property alias yesButton: yesButton
property alias noButton: noButton
height: childrenRect.height
layoutDirection: Qt.RightToLeft
spacing: 30
BlueButton {
id: yesButton;
text: yesText;
onClicked: {
console.debug('bluebutton.clicked', onYesClicked);
if(onYesClicked) {
onYesClicked();
}
}
}
WhiteButton {
id: noButton
text: noText;
onClicked: {
console.debug('whitebutton.clicked', onNoClicked);
if(onNoClicked) {
onNoClicked();
}
}
}
}

View file

@ -0,0 +1,23 @@
import "../../controls" as HifiControls
import "../../styles-uit"
import QtQuick 2.0
import QtQuick.Controls 2.2
TextField {
id: control
font.family: "Fira Sans"
font.pixelSize: 15;
color: 'black'
AvatarAppStyle {
id: style
}
background: Rectangle {
implicitWidth: 200
implicitHeight: 40
color: style.colors.inputFieldBackground
border.color: style.colors.lightGray
}
}

View file

@ -0,0 +1,177 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root;
visible: false;
anchors.fill: parent;
color: Qt.rgba(0, 0, 0, 0.5);
z: 999;
property string titleText: ''
property string bodyText: ''
property alias inputText: input;
property string imageSource: null
onImageSourceChanged: {
console.debug('imageSource = ', imageSource)
}
property string button1color: hifi.buttons.noneBorderlessGray;
property string button1text: ''
property string button2color: hifi.buttons.blue;
property string button2text: ''
property var onButton2Clicked;
property var onButton1Clicked;
function open() {
visible = true;
}
function close() {
visible = false;
onButton1Clicked = null;
onButton2Clicked = null;
button1text = '';
button2text = '';
imageSource = null;
inputText.visible = false;
inputText.placeholderText = '';
inputText.text = '';
}
HifiConstants {
id: hifi
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
}
Rectangle {
id: mainContainer;
width: Math.max(parent.width * 0.8, 400)
property int margin: 30;
height: childrenRect.height + margin * 2
onHeightChanged: {
console.debug('mainContainer: height = ', height)
}
anchors.centerIn: parent
color: "white"
TextStyle1 {
id: title
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 30
anchors.leftMargin: 30
anchors.rightMargin: 30
text: root.titleText
}
Item {
id: contentContainer
width: parent.width - 60
height: childrenRect.height
onHeightChanged: {
console.debug('contentContainer: height = ', height,
'image.height = ', image.height,
'body.height = ', body.height
)
}
anchors.top: title.bottom
anchors.topMargin: 10
anchors.left: parent.left;
anchors.leftMargin: 30;
anchors.right: parent.right;
anchors.rightMargin: 30;
TextStyle3 {
id: body;
text: root.bodyText;
anchors.left: parent.left;
anchors.right: parent.right;
height: paintedHeight;
verticalAlignment: Text.AlignTop;
wrapMode: Text.WordWrap;
}
Image {
id: image
Binding on height {
when: imageSource === null
value: 0
}
anchors.top: body.bottom
anchors.topMargin: imageSource === null ? 0 : 30
anchors.left: parent.left;
anchors.right: parent.right;
Binding on source {
when: imageSource !== null
value: imageSource
}
visible: imageSource !== null ? true : false
}
InputTextStyle4 {
id: input
visible: false
height: visible ? implicitHeight : 0
anchors.top: imageSource !== null ? image.bottom : body.bottom
anchors.left: parent.left;
anchors.right: parent.right;
}
}
DialogButtons {
id: buttons
anchors.top: contentContainer.bottom
anchors.topMargin: 30
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 30
yesButton.enabled: !input.visible || input.text.length !== 0
yesText: root.button2text
noText: root.button1text
onYesClicked: function() {
if(onButton2Clicked) {
onButton2Clicked();
} else {
root.close();
}
}
onNoClicked: function() {
if(onButton1Clicked) {
onButton1Clicked();
} else {
root.close();
}
}
}
}
}

View file

@ -0,0 +1,304 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: settings
color: 'white'
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.bottom: parent.bottom
visible: false;
z: 3
property alias onSaveClicked: dialogButtons.onYesClicked
property alias onCancelClicked: dialogButtons.onNoClicked
function open() {
visible = true;
}
function close() {
visible = false
}
Item {
anchors.left: parent.left
anchors.leftMargin: 27
anchors.top: parent.top
anchors.topMargin: 25
anchors.right: parent.right
anchors.rightMargin: 32
anchors.bottom: parent.bottom
anchors.bottomMargin: 57
RowLayout {
id: avatarScaleRow
anchors.left: parent.left
anchors.right: parent.right
spacing: 17
RalewaySemiBold {
size: 14;
text: "Avatar Scale"
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
anchors.verticalCenter: parent.verticalCenter
Layout.fillWidth: true
spacing: 0
HiFiGlyphs {
size: 30
text: 'T'
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
}
Slider {
id: slider
from: 0
to: 100
anchors.verticalCenter: parent.verticalCenter
Layout.fillWidth: true
handle: Rectangle {
width: 18
height: 18
color: 'white'
radius: 9
border.width: 1
border.color: 'black'
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
}
background: Rectangle {
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 200
implicitHeight: 18
width: slider.availableWidth
height: implicitHeight
radius: 9
border.color: 'black'
border.width: 1
color: '#f2f2f2'
}
}
HiFiGlyphs {
size: 40
text: 'T'
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
}
}
ShadowRectangle {
width: 28
height: 28
color: 'white'
radius: 3
border.color: 'black'
border.width: 1.5
anchors.verticalCenter: parent.verticalCenter
RalewaySemiBold {
size: 13;
text: "1x"
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
GridLayout {
id: handAndCollisions
anchors.top: avatarScaleRow.bottom
anchors.topMargin: 39
anchors.left: parent.left
anchors.right: parent.right
rows: 2
rowSpacing: 25
columns: 3
RalewaySemiBold {
Layout.row: 0
Layout.column: 0
size: 14;
text: "Dominant Hand"
}
ButtonGroup {
id: leftRight
}
HifiControlsUit.RadioButton {
id: leftHandRadioButton
Layout.row: 0
Layout.column: 1
Layout.leftMargin: -18
ButtonGroup.group: leftRight
checked: true
text: "Left hand"
boxSize: 20
contentItem: TextStyle9 {
text: leftHandRadioButton.text
color: 'black'
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: leftHandRadioButton.indicator.width + leftHandRadioButton.spacing
}
}
HifiControlsUit.RadioButton {
id: rightHandRadioButton
Layout.row: 0
Layout.column: 2
ButtonGroup.group: leftRight
text: "Right hand"
boxSize: 20
contentItem: TextStyle9 {
text: rightHandRadioButton.text
color: 'black'
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: rightHandRadioButton.indicator.width + rightHandRadioButton.spacing
}
}
RalewaySemiBold {
Layout.row: 1
Layout.column: 0
size: 14;
text: "Avatar Collisions"
}
ButtonGroup {
id: onOff
}
HifiControlsUit.RadioButton {
id: onRadioButton
Layout.row: 1
Layout.column: 1
Layout.leftMargin: -18
ButtonGroup.group: onOff
checked: true
text: "ON"
boxSize: 20
contentItem: TextStyle9 {
text: onRadioButton.text
color: 'black'
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: onRadioButton.indicator.width + onRadioButton.spacing
}
}
HifiControlsUit.RadioButton {
id: offRadioButton
Layout.row: 1
Layout.column: 2
ButtonGroup.group: onOff
text: "OFF"
boxSize: 20
contentItem: TextStyle9 {
text: offRadioButton.text
color: 'black'
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: offRadioButton.indicator.width + offRadioButton.spacing
}
}
}
ColumnLayout {
id: avatarAnimationLayout
anchors.top: handAndCollisions.bottom
anchors.topMargin: 25
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
RalewaySemiBold {
size: 14;
text: "Avatar Animation JSON"
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
InputTextStyle4 {
anchors.left: parent.left
anchors.right: parent.right
placeholderText: 'user\\file\\dir'
}
}
ColumnLayout {
id: avatarCollisionLayout
anchors.top: avatarAnimationLayout.bottom
anchors.topMargin: 25
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
RalewaySemiBold {
size: 14;
text: "Avatar collision sound URL (optional)"
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
InputTextStyle4 {
anchors.left: parent.left
anchors.right: parent.right
placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-'
}
}
DialogButtons {
id: dialogButtons
anchors.right: parent.right
anchors.bottom: parent.bottom
yesText: "SAVE"
noText: "CANCEL"
}
}
}

View file

@ -0,0 +1,26 @@
import "../../styles-uit"
import QtQuick 2.9
import QtGraphicalEffects 1.0
Item {
property alias source: image.source
property alias dropShadowRadius: shadow.radius
property alias dropShadowHorizontalOffset: shadow.horizontalOffset
property alias dropShadowVerticalOffset: shadow.verticalOffset
Image {
id: image
width: parent.width
height: parent.height
}
DropShadow {
id: shadow
anchors.fill: image
radius: 6
horizontalOffset: 0
verticalOffset: 3
color: Qt.rgba(0, 0, 0, 0.25)
source: image
}
}

View file

@ -0,0 +1,24 @@
import "../../styles-uit"
import QtQuick 2.9
import QtGraphicalEffects 1.0
Item {
property alias color: rectangle.color
property alias border: rectangle.border
property alias radius: rectangle.radius
Rectangle {
id: rectangle
width: parent.width
height: parent.height
}
DropShadow {
anchors.fill: rectangle
radius: 6
horizontalOffset: 0
verticalOffset: 3
color: Qt.rgba(0, 0, 0, 0.25)
source: rectangle
}
}

View file

@ -0,0 +1,21 @@
import "../../styles-uit"
import QtQuick 2.9
import QtGraphicalEffects 1.0
ShadowRectangle {
width: 44
height: 28
color: 'white'
property alias glyphText: glyph.text
property alias glyphRotation: glyph.rotation
radius: 3
border.color: 'black'
border.width: 1.5
HiFiGlyphs {
id: glyph
anchors.centerIn: parent
size: 30
}
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewaySemiBold {
size: 24;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayBold {
size: 12;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayRegular {
size: 15;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayBold {
size: 15;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayRegular {
size: 18;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
FiraSansRegular {
size: 15;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayBold {
size: 18;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewayLight {
size: 18;
}

View file

@ -0,0 +1,7 @@
import "../../controls" as HifiControls
import "../../styles-uit"
FiraSansRegular {
size: 18;
// lineHeight: 16.9;
}

View file

@ -0,0 +1,6 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewaySemiBold {
size: 20;
}

View file

@ -0,0 +1,7 @@
import "../../controls" as HifiControls
import "../../styles-uit"
RalewaySemiBold {
size: 14;
font.letterSpacing: 1.1;
}

View file

@ -0,0 +1,47 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
Row {
width: parent.width
height: xspinner.controlHeight
property int spinboxSpace: 10
property int spinboxWidth: (parent.width - 2 * spinboxSpace) / 3
property color backgroundColor: "darkgray"
spacing: spinboxSpace
property real xvalue: xspinner.value
property real yvalue: yspinner.value
property real zvalue: zspinner.value
HifiControlsUit.SpinBox {
id: xspinner
width: parent.spinboxWidth
labelInside: "X:"
backgroundColor: parent.backgroundColor
colorLabelInside: hifi.colors.redHighlight
colorScheme: hifi.colorSchemes.light
}
HifiControlsUit.SpinBox {
id: yspinner
width: parent.spinboxWidth
labelInside: "Y:"
backgroundColor: parent.backgroundColor
colorLabelInside: hifi.colors.greenHighlight
colorScheme: hifi.colorSchemes.light
}
HifiControlsUit.SpinBox {
id: zspinner
width: parent.spinboxWidth
labelInside: "Z:"
backgroundColor: parent.backgroundColor
colorLabelInside: hifi.colors.primaryHighlight
colorScheme: hifi.colorSchemes.light
}
}

View file

@ -0,0 +1,13 @@
import QtQuick 2.5
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
HifiControlsUit.Button {
HifiConstants {
id: hifi
}
color: hifi.buttons.noneBorderlessGray;
colorScheme: hifi.colorSchemes.light;
height: 40
}

View file

@ -21,6 +21,7 @@ var DEFAULT_SCRIPTS_COMBINED = [
"system/bubble.js",
"system/snapshot.js",
"system/pal.js", // "system/mod.js", // older UX, if you prefer
"system/avatarapp.js",
"system/makeUserConnection.js",
"system/tablet-goto.js",
"system/marketplaces/marketplaces.js",

910
scripts/system/avatarapp.js Normal file
View file

@ -0,0 +1,910 @@
"use strict";
/*jslint vars:true, plusplus:true, forin:true*/
/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
//
// pal.js
//
// Created by Howard Stearns on December 9, 2016
// Copyright 2016 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 request = Script.require('request').request;
var populateNearbyUserList, color, textures, removeOverlays,
controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged,
receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL,
createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged,
avatarAdded, avatarRemoved, avatarSessionChanged; // forward references;
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed
// something, will revisit as this is sorta horrible.
var UNSELECTED_TEXTURES = {
"idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"),
"idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png")
};
var SELECTED_TEXTURES = {
"idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"),
"idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png")
};
var HOVER_TEXTURES = {
"idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"),
"idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png")
};
var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6};
var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29};
var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now
var PAL_QML_SOURCE = "hifi/AvatarApp.qml";
var conserveResources = true;
Script.include("/~/system/libraries/controllers.js");
function projectVectorOntoPlane(normalizedVector, planeNormal) {
return Vec3.cross(planeNormal, Vec3.cross(normalizedVector, planeNormal));
}
function angleBetweenVectorsInPlane(from, to, normal) {
var projectedFrom = projectVectorOntoPlane(from, normal);
var projectedTo = projectVectorOntoPlane(to, normal);
return Vec3.orientedAngle(projectedFrom, projectedTo, normal);
}
//
// Overlays.
//
var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier.
function ExtendedOverlay(key, type, properties, selected, hasModel) { // A wrapper around overlays to store the key it is associated with.
overlays[key] = this;
if (hasModel) {
var modelKey = key + "-m";
this.model = new ExtendedOverlay(modelKey, "model", {
url: Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx"),
textures: textures(selected),
ignoreRayIntersection: true
}, false, false);
} else {
this.model = undefined;
}
this.key = key;
this.selected = selected || false; // not undefined
this.hovering = false;
this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected...
}
// Instance methods:
ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay
Overlays.deleteOverlay(this.activeOverlay);
delete overlays[this.key];
};
ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay
Overlays.editOverlay(this.activeOverlay, properties);
};
function color(selected, hovering, level) {
var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR;
function scale(component) {
var delta = 0xFF - component;
return component + (delta * level);
}
return {red: scale(base.red), green: scale(base.green), blue: scale(base.blue)};
}
function textures(selected, hovering) {
return hovering ? HOVER_TEXTURES : selected ? SELECTED_TEXTURES : UNSELECTED_TEXTURES;
}
// so we don't have to traverse the overlays to get the last one
var lastHoveringId = 0;
ExtendedOverlay.prototype.hover = function (hovering) {
this.hovering = hovering;
if (this.key === lastHoveringId) {
if (hovering) {
return;
}
lastHoveringId = 0;
}
this.editOverlay({color: color(this.selected, hovering, this.audioLevel)});
if (this.model) {
this.model.editOverlay({textures: textures(this.selected, hovering)});
}
if (hovering) {
// un-hover the last hovering overlay
if (lastHoveringId && lastHoveringId !== this.key) {
ExtendedOverlay.get(lastHoveringId).hover(false);
}
lastHoveringId = this.key;
}
};
ExtendedOverlay.prototype.select = function (selected) {
if (this.selected === selected) {
return;
}
UserActivityLogger.palAction(selected ? "avatar_selected" : "avatar_deselected", this.key);
this.editOverlay({color: color(selected, this.hovering, this.audioLevel)});
if (this.model) {
this.model.editOverlay({textures: textures(selected)});
}
this.selected = selected;
};
// Class methods:
var selectedIds = [];
ExtendedOverlay.isSelected = function (id) {
return -1 !== selectedIds.indexOf(id);
};
ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier
return overlays[key];
};
ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy.
var key;
for (key in overlays) {
if (iterator(ExtendedOverlay.get(key))) {
return;
}
}
};
ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any)
if (lastHoveringId) {
ExtendedOverlay.get(lastHoveringId).hover(false);
}
};
// hit(overlay) on the one overlay intersected by pickRay, if any.
// noHit() if no ExtendedOverlay was intersected (helps with hover)
ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) {
var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones.
if (!pickedOverlay.intersects) {
if (noHit) {
return noHit();
}
return;
}
ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours.
if ((overlay.activeOverlay) === pickedOverlay.overlayID) {
hit(overlay);
return true;
}
});
};
//
// Similar, for entities
//
function HighlightedEntity(id, entityProperties) {
this.id = id;
this.overlay = Overlays.addOverlay('cube', {
position: entityProperties.position,
rotation: entityProperties.rotation,
dimensions: entityProperties.dimensions,
solid: false,
color: {
red: 0xF3,
green: 0x91,
blue: 0x29
},
ignoreRayIntersection: true,
drawInFront: false // Arguable. For now, let's not distract with mysterious wires around the scene.
});
HighlightedEntity.overlays.push(this);
}
HighlightedEntity.overlays = [];
HighlightedEntity.clearOverlays = function clearHighlightedEntities() {
HighlightedEntity.overlays.forEach(function (highlighted) {
Overlays.deleteOverlay(highlighted.overlay);
});
HighlightedEntity.overlays = [];
};
HighlightedEntity.updateOverlays = function updateHighlightedEntities() {
HighlightedEntity.overlays.forEach(function (highlighted) {
var properties = Entities.getEntityProperties(highlighted.id, ['position', 'rotation', 'dimensions']);
Overlays.editOverlay(highlighted.overlay, {
position: properties.position,
rotation: properties.rotation,
dimensions: properties.dimensions
});
});
};
/* this contains current gain for a given node (by session id). More efficient than
* querying it, plus there isn't a getGain function so why write one */
var sessionGains = {};
function convertDbToLinear(decibels) {
// +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc...
// but, your perception is that something 2x as loud is +10db
// so we go from -60 to +20 or 1/64x to 4x. For now, we can
// maybe scale the signal this way??
return Math.pow(2, decibels / 10.0);
}
function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
var data;
switch (message.method) {
case 'selected':
selectedIds = message.params;
ExtendedOverlay.some(function (overlay) {
var id = overlay.key;
var selected = ExtendedOverlay.isSelected(id);
overlay.select(selected);
});
HighlightedEntity.clearOverlays();
if (selectedIds.length) {
Entities.findEntitiesInFrustum(Camera.frustum).forEach(function (id) {
// Because lastEditedBy is per session, the vast majority of entities won't match,
// so it would probably be worth reducing marshalling costs by asking for just we need.
// However, providing property name(s) is advisory and some additional properties are
// included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation',
// and 'dimensions', too, so we might as well make use of them instead of making a second
// getEntityProperties call.
// It would be nice if we could harden this against future changes by specifying all
// and only these four in an array, but see
// https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work
var properties = Entities.getEntityProperties(id, 'lastEditedBy');
if (ExtendedOverlay.isSelected(properties.lastEditedBy)) {
new HighlightedEntity(id, properties);
}
});
}
break;
case 'refreshNearby':
data = {};
ExtendedOverlay.some(function (overlay) { // capture the audio data
data[overlay.key] = overlay;
});
removeOverlays();
// If filter is specified from .qml instead of through settings, update the settings.
if (message.params.filter !== undefined) {
Settings.setValue('pal/filtered', !!message.params.filter);
}
populateNearbyUserList(message.params.selected, data);
UserActivityLogger.palAction("refresh_nearby", "");
break;
case 'refreshConnections':
print('Refreshing Connections...');
getConnectionData(false);
UserActivityLogger.palAction("refresh_connections", "");
break;
case 'removeConnection':
connectionUserName = message.params;
request({
uri: METAVERSE_BASE + '/api/v1/user/connections/' + connectionUserName,
method: 'DELETE'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to remove connection", connectionUserName, error || response.status);
return;
}
getConnectionData(false);
});
break
case 'removeFriend':
friendUserName = message.params;
print("Removing " + friendUserName + " from friends.");
request({
uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName,
method: 'DELETE'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to unfriend " + friendUserName, error || response.status);
return;
}
getConnectionData(friendUserName);
});
break
case 'addFriend':
friendUserName = message.params;
print("Adding " + friendUserName + " to friends.");
request({
uri: METAVERSE_BASE + '/api/v1/user/friends',
method: 'POST',
json: true,
body: {
username: friendUserName,
}
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to friend " + friendUserName, error || response.status);
return;
}
getConnectionData(friendUserName);
}
);
break;
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
}
}
function sendToQml(message) {
tablet.sendToQml(message);
}
function updateUser(data) {
print('PAL update:', JSON.stringify(data));
sendToQml({ method: 'updateUsername', params: data });
}
//
// User management services
//
// These are prototype versions that will be changed when the back end changes.
var METAVERSE_BASE = Account.metaverseServerURL;
function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise.
request({
uri: url
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to get", url, error || response.status);
return;
}
callback(response.data);
});
}
function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise)
// FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself
request({
uri: METAVERSE_BASE + '/users/' + username
}, function (error, html) {
var matched = !error && html.match(/img class="users-img" src="([^"]*)"/);
if (!matched) {
print('Error: Unable to get profile picture for', username, error);
callback('');
return;
}
callback(matched[1]);
});
}
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
url = METAVERSE_BASE + '/api/v1/users?'
if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces
} else {
url += 'filter=connections'; // regardless of whether online
}
requestJSON(url, function (connectionsData) {
callback(connectionsData.users);
});
}
function getInfoAboutUser(specificUsername, callback) {
url = METAVERSE_BASE + '/api/v1/users?filter=connections'
requestJSON(url, function (connectionsData) {
for (user in connectionsData.users) {
if (connectionsData.users[user].username === specificUsername) {
callback(connectionsData.users[user]);
return;
}
}
callback(false);
});
}
function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
function frob(user) { // get into the right format
var formattedSessionId = user.location.node_id || '';
if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
formattedSessionId = "{" + formattedSessionId + "}";
}
return {
sessionId: formattedSessionId,
userName: user.username,
connection: user.connection,
profileUrl: user.images.thumbnail,
placeName: (user.location.root || user.location.domain || {}).name || ''
};
}
if (specificUsername) {
getInfoAboutUser(specificUsername, function (user) {
if (user) {
updateUser(frob(user));
} else {
print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!');
}
});
} else {
getAvailableConnections(domain, function (users) {
if (domain) {
users.forEach(function (user) {
updateUser(frob(user));
});
} else {
sendToQml({ method: 'connections', params: users.map(frob) });
}
});
}
}
//
// Main operations.
//
function addAvatarNode(id) {
var selected = ExtendedOverlay.isSelected(id);
return new ExtendedOverlay(id, "sphere", {
drawInFront: true,
solid: true,
alpha: 0.8,
color: color(selected, false, 0.0),
ignoreRayIntersection: false
}, selected, !conserveResources);
}
// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter.
var avatarsOfInterest = {};
function populateNearbyUserList(selectData, oldAudioData) {
var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')},
data = [],
avatars = AvatarList.getAvatarIdentifiers(),
myPosition = filter && Camera.position,
frustum = filter && Camera.frustum,
verticalHalfAngle = filter && (frustum.fieldOfView / 2),
horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio),
orientation = filter && Camera.orientation,
forward = filter && Quat.getForward(orientation),
verticalAngleNormal = filter && Quat.getRight(orientation),
horizontalAngleNormal = filter && Quat.getUp(orientation);
avatarsOfInterest = {};
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id);
var name = avatar.sessionDisplayName;
if (!name) {
// Either we got a data packet but no identity yet, or something is really messed up. In any case,
// we won't be able to do anything with this user, so don't include them.
// In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded,
// we could be losing and gaining people randomly.
print('No avatar identity data for', id);
return;
}
if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) {
return;
}
var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition));
var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal);
var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal);
if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) {
return;
}
var oldAudio = oldAudioData && oldAudioData[id];
var avatarPalDatum = {
profileUrl: '',
displayName: name,
userName: '',
connection: '',
sessionId: id || '',
audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0,
avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0,
admin: false,
personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
ignore: !!id && Users.getIgnoreStatus(id), // ditto
isPresent: true,
isReplicated: avatar.isReplicated
};
// Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin.
Users.requestUsernameFromID(id);
if (id) {
addAvatarNode(id); // No overlay for ourselves
avatarsOfInterest[id] = true;
} else {
// Return our username from the Account API
avatarPalDatum.userName = Account.username;
}
data.push(avatarPalDatum);
print('PAL data:', JSON.stringify(avatarPalDatum));
});
getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
conserveResources = Object.keys(avatarsOfInterest).length > 20;
sendToQml({ method: 'nearbyUsers', params: data });
if (selectData) {
selectData[2] = true;
sendToQml({ method: 'select', params: selectData });
}
}
// The function that handles the reply from the server
function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
var data = {
sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially.
// If we get username (e.g., if in future we receive it when we're friends), use it.
// Otherwise, use valid machineFingerprint (which is not valid when not an admin).
userName: username || (Users.canKick && machineFingerprint) || '',
admin: isAdmin
};
// Ship the data off to QML
updateUser(data);
}
var pingPong = true;
function updateOverlays() {
var eye = Camera.position;
AvatarList.getAvatarIdentifiers().forEach(function (id) {
if (!id || !avatarsOfInterest[id]) {
return; // don't update ourself, or avatars we're not interested in
}
var avatar = AvatarList.getAvatar(id);
if (!avatar) {
return; // will be deleted below if there had been an overlay.
}
var overlay = ExtendedOverlay.get(id);
if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back.
print('Adding non-PAL avatar node', id);
overlay = addAvatarNode(id);
}
var target = avatar.position;
var distance = Vec3.distance(target, eye);
var offset = 0.2;
var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position)
var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can
if (headIndex > 0) {
offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2;
}
// move a bit in front, towards the camera
target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset));
// now bump it up a bit
target.y = target.y + offset;
overlay.ping = pingPong;
overlay.editOverlay({
color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel),
position: target,
dimensions: 0.032 * distance
});
if (overlay.model) {
overlay.model.ping = pingPong;
overlay.model.editOverlay({
position: target,
scale: 0.2 * distance, // constant apparent size
rotation: Camera.orientation
});
}
});
pingPong = !pingPong;
ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.)
if (overlay.ping === pingPong) {
overlay.deleteOverlay();
}
});
// We could re-populateNearbyUserList if anything added or removed, but not for now.
HighlightedEntity.updateOverlays();
}
function removeOverlays() {
selectedIds = [];
lastHoveringId = 0;
HighlightedEntity.clearOverlays();
ExtendedOverlay.some(function (overlay) {
overlay.deleteOverlay();
});
}
//
// Clicks.
//
function handleClick(pickRay) {
ExtendedOverlay.applyPickRay(pickRay, function (overlay) {
// Don't select directly. Tell qml, who will give us back a list of ids.
var message = {method: 'select', params: [[overlay.key], !overlay.selected, false]};
sendToQml(message);
return true;
});
}
function handleMouseEvent(mousePressEvent) { // handleClick if we get one.
if (!mousePressEvent.isLeftButton) {
return;
}
handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y));
}
function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic
ExtendedOverlay.applyPickRay(pickRay, function (overlay) {
overlay.hover(true);
}, function () {
ExtendedOverlay.unHover();
});
}
// handy global to keep track of which hand is the mouse (if any)
var currentHandPressed = 0;
var TRIGGER_CLICK_THRESHOLD = 0.85;
var TRIGGER_PRESS_THRESHOLD = 0.05;
function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position
var pickRay;
if (HMD.active) {
if (currentHandPressed !== 0) {
pickRay = controllerComputePickRay(currentHandPressed);
} else {
// nothing should hover, so
ExtendedOverlay.unHover();
return;
}
} else {
pickRay = Camera.computePickRay(event.x, event.y);
}
handleMouseMove(pickRay);
}
function handleTriggerPressed(hand, value) {
// The idea is if you press one trigger, it is the one
// we will consider the mouse. Even if the other is pressed,
// we ignore it until this one is no longer pressed.
var isPressed = value > TRIGGER_PRESS_THRESHOLD;
if (currentHandPressed === 0) {
currentHandPressed = isPressed ? hand : 0;
return;
}
if (currentHandPressed === hand) {
currentHandPressed = isPressed ? hand : 0;
return;
}
// otherwise, the other hand is still triggered
// so do nothing.
}
// We get mouseMoveEvents from the handControllers, via handControllerPointer.
// But we don't get mousePressEvents.
var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click');
var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press');
function controllerComputePickRay(hand) {
var controllerPose = getControllerWorldLocation(hand, true);
if (controllerPose.valid) {
return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) };
}
}
function makeClickHandler(hand) {
return function (clicked) {
if (clicked > TRIGGER_CLICK_THRESHOLD) {
var pickRay = controllerComputePickRay(hand);
handleClick(pickRay);
}
};
}
function makePressHandler(hand) {
return function (value) {
handleTriggerPressed(hand, value);
};
}
triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
//
// Manage the connection between the button and the window.
//
var button;
var buttonName = "AvatarApp";
var tablet = null;
function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
button = tablet.addButton({
text: buttonName,
icon: "icons/tablet-icons/people-i.svg",
activeIcon: "icons/tablet-icons/people-a.svg",
sortOrder: 7
});
button.clicked.connect(onTabletButtonClicked);
tablet.screenChanged.connect(onTabletScreenChanged);
Window.domainChanged.connect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL);
Messages.messageReceived.connect(receiveMessage);
Users.avatarDisconnected.connect(avatarDisconnected);
AvatarList.avatarAddedEvent.connect(avatarAdded);
AvatarList.avatarRemovedEvent.connect(avatarRemoved);
AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged);
}
startup();
var isWired = false;
var audioTimer;
var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too)
var AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS = 300;
function off() {
if (isWired) { // It is not ok to disconnect these twice, hence guard.
Script.update.disconnect(updateOverlays);
Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
isWired = false;
ContextOverlay.enabled = true
}
if (audioTimer) {
Script.clearInterval(audioTimer);
}
triggerMapping.disable(); // It's ok if we disable twice.
triggerPressMapping.disable(); // see above
removeOverlays();
Users.requestsDomainListData = false;
}
function tabletVisibilityChanged() {
if (!tablet.tabletShown) {
ContextOverlay.enabled = true;
tablet.gotoHomeScreen();
}
}
var onPalScreen = false;
function onTabletButtonClicked() {
if (onPalScreen) {
// for toolbar-mode: go back to home screen, this will close the window.
tablet.gotoHomeScreen();
ContextOverlay.enabled = true;
} else {
ContextOverlay.enabled = false;
tablet.loadQMLSource(PAL_QML_SOURCE);
tablet.tabletShownChanged.connect(tabletVisibilityChanged);
Users.requestsDomainListData = true;
populateNearbyUserList();
isWired = true;
Script.update.connect(updateOverlays);
Controller.mousePressEvent.connect(handleMouseEvent);
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
Users.usernameFromIDReply.connect(usernameFromIDReply);
triggerMapping.enable();
triggerPressMapping.enable();
audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS);
}
}
var hasEventBridge = false;
function wireEventBridge(on) {
if (on) {
if (!hasEventBridge) {
tablet.fromQml.connect(fromQml);
hasEventBridge = true;
}
} else {
if (hasEventBridge) {
tablet.fromQml.disconnect(fromQml);
hasEventBridge = false;
}
}
}
function onTabletScreenChanged(type, url) {
onPalScreen = (type === "QML" && url === PAL_QML_SOURCE);
wireEventBridge(onPalScreen);
// for toolbar mode: change button to active when window is first openend, false otherwise.
button.editProperties({isActive: onPalScreen});
// disable sphere overlays when not on pal screen.
if (!onPalScreen) {
off();
}
}
//
// Message from other scripts, such as edit.js
//
var CHANNEL = 'com.highfidelity.pal';
function receiveMessage(channel, messageString, senderID) {
if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) {
return;
}
var message = JSON.parse(messageString);
switch (message.method) {
case 'select':
sendToQml(message); // Accepts objects, not just strings.
break;
default:
print('Unrecognized PAL message', messageString);
}
}
var AVERAGING_RATIO = 0.05;
var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0;
var LOG2 = Math.log(2.0);
var AUDIO_PEAK_DECAY = 0.02;
var myData = {}; // we're not includied in ExtendedOverlay.get.
function scaleAudio(val) {
var audioLevel = 0.0;
if (val <= LOUDNESS_FLOOR) {
audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE;
} else {
audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE;
}
if (audioLevel > 1.0) {
audioLevel = 1;
}
return audioLevel;
}
function getAudioLevel(id) {
// the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged
// But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency
// of updating (the latter for efficiency too).
var avatar = AvatarList.getAvatar(id);
var audioLevel = 0.0;
var avgAudioLevel = 0.0;
var data = id ? ExtendedOverlay.get(id) : myData;
if (data) {
// we will do exponential moving average by taking some the last loudness and averaging
data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness);
// add 1 to insure we don't go log() and hit -infinity. Math.log is
// natural log, so to get log base 2, just divide by ln(2).
audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2);
// decay avgAudioLevel
avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel);
data.avgAudioLevel = avgAudioLevel;
data.audioLevel = audioLevel;
// now scale for the gain. Also, asked to boost the low end, so one simple way is
// to take sqrt of the value. Lets try that, see how it feels.
avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75)));
}
return [audioLevel, avgAudioLevel];
}
function createAudioInterval(interval) {
// we will update the audioLevels periodically
// TODO: tune for efficiency - expecially with large numbers of avatars
return Script.setInterval(function () {
var param = {};
AvatarList.getAvatarIdentifiers().forEach(function (id) {
var level = getAudioLevel(id),
userId = id || 0; // qml didn't like an object with null/empty string for a key, so...
param[userId] = level;
});
sendToQml({method: 'updateAudioLevel', params: param});
}, interval);
}
function avatarDisconnected(nodeID) {
// remove from the pal list
sendToQml({method: 'avatarDisconnected', params: [nodeID]});
}
function clearLocalQMLDataAndClosePAL() {
sendToQml({ method: 'clearLocalQMLData' });
if (onPalScreen) {
ContextOverlay.enabled = true;
tablet.gotoHomeScreen();
}
}
function avatarAdded(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] });
}
function avatarRemoved(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] });
}
function avatarSessionChanged(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] });
}
function shutdown() {
if (onPalScreen) {
tablet.gotoHomeScreen();
}
button.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(button);
tablet.screenChanged.disconnect(onTabletScreenChanged);
Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL);
Messages.messageReceived.disconnect(receiveMessage);
Users.avatarDisconnected.disconnect(avatarDisconnected);
AvatarList.avatarAddedEvent.disconnect(avatarAdded);
AvatarList.avatarRemovedEvent.disconnect(avatarRemoved);
AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged);
off();
}
//
// Cleanup.
//
Script.scriptEnding.connect(shutdown);
}()); // END LOCAL_SCOPE