wip on avatarapp
9
interface/resources/images/FavoriteIconActive.svg
Normal 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 |
8
interface/resources/images/FavoriteIconInActive.svg
Normal 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 |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 103 KiB |
BIN
interface/resources/images/samples/hifi-place-get-avatars.png
Normal file
After Width: | Height: | Size: 82 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
759
interface/resources/qml/hifi/AvatarApp.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
183
interface/resources/qml/hifi/avatarapp/AdjustWearables.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml
Normal 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"
|
||||
}
|
||||
}
|
30
interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
48
interface/resources/qml/hifi/avatarapp/AvatarsModel.qml
Normal 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
|
||||
}
|
||||
}
|
13
interface/resources/qml/hifi/avatarapp/BlueButton.qml
Normal 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
|
||||
}
|
136
interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
interface/resources/qml/hifi/avatarapp/DialogButtons.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml
Normal 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
|
||||
}
|
||||
}
|
177
interface/resources/qml/hifi/avatarapp/MessageBox.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
304
interface/resources/qml/hifi/avatarapp/Settings.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
26
interface/resources/qml/hifi/avatarapp/ShadowImage.qml
Normal 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
|
||||
}
|
||||
}
|
24
interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml
Normal 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
|
||||
}
|
||||
}
|
21
interface/resources/qml/hifi/avatarapp/SquareLabel.qml
Normal 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
|
||||
}
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle1.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewaySemiBold {
|
||||
size: 24;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle10.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayBold {
|
||||
size: 12;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle11.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayRegular {
|
||||
size: 15;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle2.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle3.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayRegular {
|
||||
size: 18;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle4.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
FiraSansRegular {
|
||||
size: 15;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle5.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayBold {
|
||||
size: 18;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle6.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewayLight {
|
||||
size: 18;
|
||||
}
|
7
interface/resources/qml/hifi/avatarapp/TextStyle7.qml
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
FiraSansRegular {
|
||||
size: 18;
|
||||
// lineHeight: 16.9;
|
||||
}
|
6
interface/resources/qml/hifi/avatarapp/TextStyle8.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewaySemiBold {
|
||||
size: 20;
|
||||
}
|
7
interface/resources/qml/hifi/avatarapp/TextStyle9.qml
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "../../controls" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
RalewaySemiBold {
|
||||
size: 14;
|
||||
font.letterSpacing: 1.1;
|
||||
}
|
47
interface/resources/qml/hifi/avatarapp/Vector3.qml
Normal 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
|
||||
}
|
||||
}
|
13
interface/resources/qml/hifi/avatarapp/WhiteButton.qml
Normal 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
|
||||
}
|
|
@ -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
|
@ -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
|