Merge pull request #10130 from ctrlaltdavid/21249

New avatar record and playback script
This commit is contained in:
anshuman64 2017-04-26 15:18:30 -07:00 committed by GitHub
commit 5bd9b2e631
27 changed files with 2691 additions and 14 deletions

View file

@ -30,6 +30,7 @@
#include <ShutdownEventListener.h>
#include <SoundCache.h>
#include <ResourceScriptingInterface.h>
#include <UserActivityLoggerScriptingInterface.h>
#include "AssignmentFactory.h"
#include "AssignmentDynamicFactory.h"
@ -66,6 +67,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<UserActivityLoggerScriptingInterface>();
// setup a thread for the NodeList and its PacketReceiver
QThread* nodeThread = new QThread(this);

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
sodipodi:docname="avatar-record-a.svg"
inkscape:version="0.92.1 r15371"><metadata
id="metadata36"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs34" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1829"
inkscape:window-height="1057"
id="namedview32"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="-9.4279661"
inkscape:cy="25"
inkscape:window-x="83"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="g879"><path
class="st0"
d="m 23.2,20.5 c -1,0.8 -1.8,1.4 -2.7,2.1 -0.2,0.1 -0.2,0.4 -0.2,0.7 -0.3,1.7 -0.6,3.4 -0.9,5.1 -0.1,0.8 -0.6,1.2 -1.3,1.1 -0.7,-0.1 -1.2,-0.7 -1.1,-1.4 0.3,-2.2 0.6,-4.4 1,-6.6 0.1,-0.3 0.3,-0.7 0.6,-0.9 1.4,-1.3 2.8,-2.5 4.2,-3.7 0.7,-0.6 1.5,-1 2.4,-0.9 0.3,0 0.7,0 1,0 1,-0.1 1.7,0.4 2.1,1.3 0.7,1.4 1.4,2.8 1.9,4.3 0.5,1.3 1.2,2.1 2.4,2.6 1,0.4 2,1 3,1.5 0.2,0.1 0.5,0.3 0.7,0.5 0.4,0.4 0.5,1 0.3,1.4 C 36.4,28 36,28.1 35.5,28 35.1,27.9 34.7,27.8 34.3,27.6 33,27 31.8,26.4 30.6,25.8 29.8,25.5 29.2,25 28.8,24.2 c -0.2,-0.3 -0.4,-0.6 -0.7,-1 -0.1,0.3 -0.1,0.5 -0.2,0.7 -0.3,1.2 -0.5,2.4 -0.8,3.6 -0.1,0.4 0,0.7 0.2,1 2.2,3.7 4.4,7.4 6.6,11.1 0.3,0.4 0.4,1 0.5,1.5 0.1,0.7 -0.1,1.3 -0.7,1.6 C 33,43.1 32.3,43.1 31.8,42.6 31.4,42.2 31,41.8 30.7,41.3 28.2,37.4 25.7,33.4 23.2,29.5 22.8,28.8 22.4,28 22.1,27.3 22,26.9 22,26.4 22.1,26 c 0.4,-1.8 0.7,-3.6 1.1,-5.5 z"
id="path5"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M 23.2,33.9 C 23.1,33.8 23,33.7 23,33.6 c 0,0 0,0 0,0 -0.2,-0.2 -0.3,-0.5 -0.5,-0.7 -0.3,-0.4 -0.6,-0.8 -0.9,-1.1 -0.3,1 -0.5,2 -0.8,3 -0.1,0.3 -0.3,0.7 -0.4,1 -1,1.5 -2,3.1 -3,4.6 -0.2,0.4 -0.4,0.8 -0.6,1.3 -0.2,0.9 0.7,1.9 1.6,1.5 0.5,-0.2 1,-0.7 1.3,-1.1 0.9,-1.1 1.6,-2.3 2.5,-3.3 0.8,-1 1.4,-2.2 1.8,-3.4 -0.2,-0.7 -0.5,-1.1 -0.8,-1.5 z"
id="path7"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M 29,11.6 C 29,12.9 27.9,14 26.6,14 H 26.4 C 25.1,14 24,12.9 24,11.6 V 10.4 C 24,9.1 25.1,8 26.4,8 h 0.2 c 1.3,0 2.4,1.1 2.4,2.4 z"
id="path9"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="m 43.4,24.1 c -0.5,0.3 -0.9,0.5 -1.4,0.8 v 6.3 h 2.3 v -7.6 c -0.3,0.2 -0.6,0.3 -0.9,0.5 z"
id="path11"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M 42,38.6 V 39 c 0,1.2 -1,2.1 -2.1,2.1 h -0.8 v 2.3 h 0.8 c 2.5,0 4.5,-2 4.5,-4.5 V 38.5 H 42 Z"
id="path13"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="m 9.7,12.2 v -0.4 c 0,-1.2 1,-2.1 2.1,-2.1 h 2 V 7.3 h -2 c -2.5,0 -4.5,2 -4.5,4.5 v 0.4 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><rect
x="7.4000001"
y="18.299999"
class="st0"
width="2.3"
height="12.9"
id="rect17"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M 9.7,38.9 V 38.5 H 7.4 v 0.4 c 0,2.5 2,4.5 4.5,4.5 h 2 v -2.3 h -2 c -1.2,0 -2.2,-1 -2.2,-2.2 z"
id="path19"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /><g
style="fill:#000000;fill-opacity:1"
id="g25"><circle
class="st0"
cx="38.599998"
cy="13.3"
r="2.2"
id="circle21"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="m 38.6,15.5 c -1.2,0 -2.2,-1 -2.2,-2.2 0,-1.2 1,-2.2 2.2,-2.2 1.2,0 2.2,1 2.2,2.2 0,1.2 -1,2.2 -2.2,2.2 z m 0,-4.3 c -1.1,0 -2.1,0.9 -2.1,2.1 0,1.2 0.9,2.1 2.1,2.1 1.1,0 2.1,-0.9 2.1,-2.1 0,-1.2 -1,-2.1 -2.1,-2.1 z"
id="path23"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /></g><path
class="st0"
d="m 38.6,19.7 c -3.6,0 -6.4,-2.9 -6.4,-6.4 0,-3.5 2.9,-6.4 6.4,-6.4 3.6,0 6.4,2.9 6.4,6.4 0,3.5 -2.9,6.4 -6.4,6.4 z m 0,-10.6 c -2.3,0 -4.2,1.9 -4.2,4.2 0,2.3 1.9,4.2 4.2,4.2 2.3,0 4.2,-1.9 4.2,-4.2 0,-2.3 -1.9,-4.2 -4.2,-4.2 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g>
<path class="st0" d="M23.2,20.5c-1,0.8-1.8,1.4-2.7,2.1c-0.2,0.1-0.2,0.4-0.2,0.7c-0.3,1.7-0.6,3.4-0.9,5.1
c-0.1,0.8-0.6,1.2-1.3,1.1c-0.7-0.1-1.2-0.7-1.1-1.4c0.3-2.2,0.6-4.4,1-6.6c0.1-0.3,0.3-0.7,0.6-0.9c1.4-1.3,2.8-2.5,4.2-3.7
c0.7-0.6,1.5-1,2.4-0.9c0.3,0,0.7,0,1,0c1-0.1,1.7,0.4,2.1,1.3c0.7,1.4,1.4,2.8,1.9,4.3c0.5,1.3,1.2,2.1,2.4,2.6c1,0.4,2,1,3,1.5
c0.2,0.1,0.5,0.3,0.7,0.5c0.4,0.4,0.5,1,0.3,1.4C36.4,28,36,28.1,35.5,28c-0.4-0.1-0.8-0.2-1.2-0.4c-1.3-0.6-2.5-1.2-3.7-1.8
c-0.8-0.3-1.4-0.8-1.8-1.6c-0.2-0.3-0.4-0.6-0.7-1c-0.1,0.3-0.1,0.5-0.2,0.7c-0.3,1.2-0.5,2.4-0.8,3.6c-0.1,0.4,0,0.7,0.2,1
c2.2,3.7,4.4,7.4,6.6,11.1c0.3,0.4,0.4,1,0.5,1.5c0.1,0.7-0.1,1.3-0.7,1.6c-0.7,0.4-1.4,0.4-1.9-0.1c-0.4-0.4-0.8-0.8-1.1-1.3
c-2.5-3.9-5-7.9-7.5-11.8c-0.4-0.7-0.8-1.5-1.1-2.2c-0.1-0.4-0.1-0.9,0-1.3C22.5,24.2,22.8,22.4,23.2,20.5z"/>
<path class="st0" d="M23.2,33.9c-0.1-0.1-0.2-0.2-0.2-0.3c0,0,0,0,0,0c-0.2-0.2-0.3-0.5-0.5-0.7c-0.3-0.4-0.6-0.8-0.9-1.1
c-0.3,1-0.5,2-0.8,3c-0.1,0.3-0.3,0.7-0.4,1c-1,1.5-2,3.1-3,4.6c-0.2,0.4-0.4,0.8-0.6,1.3c-0.2,0.9,0.7,1.9,1.6,1.5
c0.5-0.2,1-0.7,1.3-1.1c0.9-1.1,1.6-2.3,2.5-3.3c0.8-1,1.4-2.2,1.8-3.4C23.8,34.7,23.5,34.3,23.2,33.9z"/>
<path class="st0" d="M29,11.6c0,1.3-1.1,2.4-2.4,2.4h-0.2c-1.3,0-2.4-1.1-2.4-2.4v-1.2C24,9.1,25.1,8,26.4,8h0.2
c1.3,0,2.4,1.1,2.4,2.4V11.6z"/>
<path class="st0" d="M43.4,24.1c-0.5,0.3-0.9,0.5-1.4,0.8v6.3h2.3v-7.6C44,23.8,43.7,23.9,43.4,24.1z"/>
<path class="st0" d="M42,38.6v0.4c0,1.2-1,2.1-2.1,2.1h-0.8v2.3h0.8c2.5,0,4.5-2,4.5-4.5v-0.4H42z"/>
<path class="st0" d="M9.7,12.2v-0.4c0-1.2,1-2.1,2.1-2.1h2V7.3h-2c-2.5,0-4.5,2-4.5,4.5v0.4H9.7z"/>
<rect x="7.4" y="18.3" class="st0" width="2.3" height="12.9"/>
<path class="st0" d="M9.7,38.9v-0.4H7.4v0.4c0,2.5,2,4.5,4.5,4.5h2v-2.3h-2C10.7,41.1,9.7,40.1,9.7,38.9z"/>
<g>
<circle class="st0" cx="38.6" cy="13.3" r="2.2"/>
<path class="st0" d="M38.6,15.5c-1.2,0-2.2-1-2.2-2.2s1-2.2,2.2-2.2c1.2,0,2.2,1,2.2,2.2S39.8,15.5,38.6,15.5z M38.6,11.2
c-1.1,0-2.1,0.9-2.1,2.1s0.9,2.1,2.1,2.1c1.1,0,2.1-0.9,2.1-2.1S39.7,11.2,38.6,11.2z"/>
</g>
<path class="st0" d="M38.6,19.7c-3.6,0-6.4-2.9-6.4-6.4s2.9-6.4,6.4-6.4c3.6,0,6.4,2.9,6.4,6.4S42.1,19.7,38.6,19.7z M38.6,9.1
c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2c2.3,0,4.2-1.9,4.2-4.2S40.9,9.1,38.6,9.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -466,6 +466,11 @@ FocusScope {
return fileDialogBuilder.createObject(desktop, properties);
}
Component { id: assetDialogBuilder; AssetDialog { } }
function assetDialog(properties) {
return assetDialogBuilder.createObject(desktop, properties);
}
function unfocusWindows() {
// First find the active focus item, and unfocus it, all the way
// up the parent chain to the window

View file

@ -0,0 +1,58 @@
//
// AssetDialog.qml
//
// Created by David Rowe on 18 Apr 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import Qt.labs.settings 1.0
import "../styles-uit"
import "../windows"
import "assetDialog"
ModalWindow {
id: root
resizable: true
implicitWidth: 480
implicitHeight: 360
minSize: Qt.vector2d(360, 240)
draggable: true
Settings {
category: "AssetDialog"
property alias width: root.width
property alias height: root.height
property alias x: root.x
property alias y: root.y
}
// Set from OffscreenUi::assetDialog().
property alias caption: root.title
property alias dir: assetDialogContent.dir
property alias filter: assetDialogContent.filter
property alias options: assetDialogContent.options
// Dialog results.
signal selectedAsset(var asset);
signal canceled();
property int titleWidth: 0 // For ModalFrame.
HifiConstants { id: hifi }
AssetDialogContent {
id: assetDialogContent
width: pane.width
height: pane.height
anchors.margins: 0
}
}

View file

@ -0,0 +1,53 @@
//
// TabletAssetDialog.qml
//
// Created by David Rowe on 18 Apr 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../styles-uit"
import "../windows"
import "assetDialog"
TabletModalWindow {
id: root
anchors.fill: parent
width: parent.width
height: parent.height
// Set from OffscreenUi::assetDialog().
property alias caption: root.title
property alias dir: assetDialogContent.dir
property alias filter: assetDialogContent.filter
property alias options: assetDialogContent.options
// Dialog results.
signal selectedAsset(var asset);
signal canceled();
property int titleWidth: 0 // For TabletModalFrame.
TabletModalFrame {
id: frame
anchors.fill: parent
AssetDialogContent {
id: assetDialogContent
singleClickNavigate: true
width: parent.width - 12
height: parent.height - frame.frameMarginTop - 12
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: parent.height - height - 6
}
}
}
}

View file

@ -0,0 +1,536 @@
//
// AssetDialogContent.qml
//
// Created by David Rowe on 19 Apr 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../controls-uit"
import "../../styles-uit"
import "../fileDialog"
Item {
// Set from OffscreenUi::assetDialog()
property alias dir: assetTableModel.folder
property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx".
property int options // Not used.
property bool selectDirectory: false
// Not implemented.
//property bool saveDialog: false;
//property bool multiSelect: false;
property bool singleClickNavigate: false
HifiConstants { id: hifi }
Component.onCompleted: {
homeButton.destination = dir;
if (selectDirectory) {
d.currentSelectionIsFolder = true;
d.currentSelectionPath = assetTableModel.folder;
}
assetTableView.forceActiveFocus();
}
Item {
id: assetDialogItem
anchors.fill: parent
clip: true
MouseArea {
// Clear selection when click on internal unused area.
anchors.fill: parent
drag.target: root
onClicked: {
d.clearSelection();
frame.forceActiveFocus();
assetTableView.forceActiveFocus();
}
}
Row {
id: navControls
anchors {
top: parent.top
topMargin: hifi.dimensions.contentMargin.y
left: parent.left
}
spacing: hifi.dimensions.contentSpacing.x
GlyphButton {
id: upButton
glyph: hifi.glyphs.levelUp
width: height
size: 30
enabled: assetTableModel.parentFolder !== ""
onClicked: d.navigateUp();
}
GlyphButton {
id: homeButton
property string destination: ""
glyph: hifi.glyphs.home
size: 28
width: height
enabled: destination !== ""
//onClicked: d.navigateHome();
onClicked: assetTableModel.folder = destination;
}
}
ComboBox {
id: pathSelector
anchors {
top: parent.top
topMargin: hifi.dimensions.contentMargin.y
left: navControls.right
leftMargin: hifi.dimensions.contentSpacing.x
right: parent.right
}
z: 10
property string lastValidFolder: assetTableModel.folder
function calculatePathChoices(folder) {
var folders = folder.split("/"),
choices = [],
i, length;
if (folders[folders.length - 1] === "") {
folders.pop();
}
choices.push(folders[0]);
for (i = 1, length = folders.length; i < length; i++) {
choices.push(choices[i - 1] + "/" + folders[i]);
}
if (folders[0] === "") {
choices[0] = "/";
}
choices.reverse();
if (choices.length > 0) {
pathSelector.model = choices;
}
}
onLastValidFolderChanged: {
var folder = lastValidFolder;
calculatePathChoices(folder);
}
onCurrentTextChanged: {
var folder = currentText;
if (folder !== "/") {
folder += "/";
}
if (folder !== assetTableModel.folder) {
if (root.selectDirectory) {
currentSelection.text = currentText;
d.currentSelectionPath = currentText;
}
assetTableModel.folder = folder;
assetTableView.forceActiveFocus();
}
}
}
QtObject {
id: d
property string currentSelectionPath
property bool currentSelectionIsFolder
property var tableViewConnection: Connections { target: assetTableView; onCurrentRowChanged: d.update(); }
function update() {
var row = assetTableView.currentRow;
if (row === -1) {
if (!root.selectDirectory) {
currentSelection.text = "";
currentSelectionIsFolder = false;
}
return;
}
var rowInfo = assetTableModel.get(row);
currentSelectionPath = rowInfo.filePath;
currentSelectionIsFolder = rowInfo.fileIsDir;
if (root.selectDirectory || !currentSelectionIsFolder) {
currentSelection.text = currentSelectionPath;
} else {
currentSelection.text = "";
}
}
function navigateUp() {
if (assetTableModel.parentFolder !== "") {
assetTableModel.folder = assetTableModel.parentFolder;
return true;
}
return false;
}
function navigateHome() {
assetTableModel.folder = homeButton.destination;
return true;
}
function clearSelection() {
assetTableView.selection.clear();
assetTableView.currentRow = -1;
update();
}
}
ListModel {
id: assetTableModel
property string folder
property string parentFolder: ""
readonly property string rootFolder: "/"
onFolderChanged: {
parentFolder = calculateParentFolder();
update();
}
function calculateParentFolder() {
if (folder !== "/") {
return folder.slice(0, folder.slice(0, -1).lastIndexOf("/") + 1);
}
return "";
}
function isFolder(row) {
if (row === -1) {
return false;
}
return get(row).fileIsDir;
}
function onGetAllMappings(error, map) {
var mappings,
fileTypeFilter,
index,
path,
fileName,
fileType,
fileIsDir,
isValid,
subDirectory,
subDirectories = [],
fileNameSort,
rows = 0,
lower,
middle,
upper,
i,
length;
clear();
if (error === "") {
mappings = Object.keys(map);
fileTypeFilter = filter.replace("*", "").toLowerCase();
for (i = 0, length = mappings.length; i < length; i++) {
index = mappings[i].lastIndexOf("/");
path = mappings[i].slice(0, mappings[i].lastIndexOf("/") + 1);
fileName = mappings[i].slice(path.length);
fileType = fileName.slice(fileName.lastIndexOf("."));
fileIsDir = false;
isValid = false;
if (fileType.toLowerCase() === fileTypeFilter) {
if (path === folder) {
isValid = !selectDirectory;
} else if (path.length > folder.length) {
subDirectory = path.slice(folder.length);
index = subDirectory.indexOf("/");
if (index === subDirectory.lastIndexOf("/")) {
fileName = subDirectory.slice(0, index);
if (subDirectories.indexOf(fileName) === -1) {
fileIsDir = true;
isValid = true;
subDirectories.push(fileName);
}
}
}
}
if (isValid) {
fileNameSort = (fileIsDir ? "*" : "") + fileName.toLowerCase();
lower = 0;
upper = rows;
while (lower < upper) {
middle = Math.floor((lower + upper) / 2);
var lessThan;
if (fileNameSort < get(middle)["fileNameSort"]) {
lessThan = true;
upper = middle;
} else {
lessThan = false;
lower = middle + 1;
}
}
insert(lower, {
fileName: fileName,
filePath: path + (fileIsDir ? "" : fileName),
fileIsDir: fileIsDir,
fileNameSort: fileNameSort
});
rows++;
}
}
} else {
console.log("Error getting mappings from Asset Server");
}
}
function update() {
d.clearSelection();
clear();
Assets.getAllMappings(onGetAllMappings);
}
}
Table {
id: assetTableView
colorScheme: hifi.colorSchemes.light
anchors {
top: navControls.bottom
topMargin: hifi.dimensions.contentSpacing.y
left: parent.left
right: parent.right
bottom: currentSelection.top
bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height
}
model: assetTableModel
focus: true
onClicked: {
if (singleClickNavigate) {
navigateToRow(row);
}
}
onDoubleClicked: navigateToRow(row);
Keys.onReturnPressed: navigateToCurrentRow();
Keys.onEnterPressed: navigateToCurrentRow();
itemDelegate: Item {
clip: true
FontLoader { id: firaSansSemiBold; source: "../../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansRegular; source: "../../../fonts/FiraSans-Regular.ttf"; }
FiraSansSemiBold {
text: styleData.value
elide: styleData.elideMode
anchors {
left: parent.left
leftMargin: hifi.dimensions.tablePadding
right: parent.right
rightMargin: hifi.dimensions.tablePadding
verticalCenter: parent.verticalCenter
}
size: hifi.fontSizes.tableText
color: hifi.colors.baseGrayHighlight
font.family: (styleData.row !== -1 && assetTableView.model.get(styleData.row).fileIsDir)
? firaSansSemiBold.name : firaSansRegular.name
}
}
TableViewColumn {
id: fileNameColumn
role: "fileName"
title: "Name"
width: assetTableView.width
movable: false
resizable: false
}
function navigateToRow(row) {
currentRow = row;
navigateToCurrentRow();
}
function navigateToCurrentRow() {
if (model.isFolder(currentRow)) {
model.folder = model.get(currentRow).filePath;
} else {
okAction.trigger();
}
}
Timer {
id: prefixClearTimer
interval: 1000
repeat: false
running: false
onTriggered: assetTableView.prefix = "";
}
property string prefix: ""
function addToPrefix(event) {
if (!event.text || event.text === "") {
return false;
}
var newPrefix = prefix + event.text.toLowerCase();
var matchedIndex = -1;
for (var i = 0; i < model.count; ++i) {
var name = model.get(i).fileName.toLowerCase();
if (0 === name.indexOf(newPrefix)) {
matchedIndex = i;
break;
}
}
if (matchedIndex !== -1) {
assetTableView.selection.clear();
assetTableView.selection.select(matchedIndex);
assetTableView.currentRow = matchedIndex;
assetTableView.prefix = newPrefix;
}
prefixClearTimer.restart();
return true;
}
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Backspace:
case Qt.Key_Tab:
case Qt.Key_Backtab:
event.accepted = false;
break;
default:
if (addToPrefix(event)) {
event.accepted = true
} else {
event.accepted = false;
}
break;
}
}
}
TextField {
id: currentSelection
label: selectDirectory ? "Directory:" : "File name:"
anchors {
left: parent.left
right: selectionType.visible ? selectionType.left: parent.right
rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0
bottom: buttonRow.top
bottomMargin: hifi.dimensions.contentSpacing.y
}
readOnly: true
activeFocusOnTab: !readOnly
onActiveFocusChanged: if (activeFocus) { selectAll(); }
onAccepted: okAction.trigger();
}
FileTypeSelection {
id: selectionType
anchors {
top: currentSelection.top
left: buttonRow.left
right: parent.right
}
visible: !selectDirectory && filtersCount > 1
KeyNavigation.left: assetTableView
KeyNavigation.right: openButton
}
Action {
id: okAction
text: currentSelection.text && root.selectDirectory && assetTableView.currentRow === -1 ? "Choose" : "Open"
enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false
onTriggered: {
if (!root.selectDirectory && !d.currentSelectionIsFolder
|| root.selectDirectory && assetTableView.currentRow === -1) {
selectedAsset(d.currentSelectionPath);
root.destroy();
} else {
assetTableView.navigateToCurrentRow();
}
}
}
Action {
id: cancelAction
text: "Cancel"
onTriggered: {
canceled();
root.destroy();
}
}
Row {
id: buttonRow
anchors {
right: parent.right
bottom: parent.bottom
}
spacing: hifi.dimensions.contentSpacing.y
Button {
id: openButton
color: hifi.buttons.blue
action: okAction
Keys.onReturnPressed: okAction.trigger()
KeyNavigation.up: selectionType
KeyNavigation.left: selectionType
KeyNavigation.right: cancelButton
}
Button {
id: cancelButton
action: cancelAction
KeyNavigation.up: selectionType
KeyNavigation.left: openButton
KeyNavigation.right: assetTableView.contentItem
Keys.onReturnPressed: { canceled(); root.enabled = false }
}
}
}
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Backspace:
event.accepted = d.navigateUp();
break;
case Qt.Key_Home:
event.accepted = d.navigateHome();
break;
}
}
}

View file

@ -44,6 +44,12 @@ Item {
return openModal;
}
Component { id: assetDialogBuilder; TabletAssetDialog { } }
function assetDialog(properties) {
openModal = assetDialogBuilder.createObject(tabletRoot, properties);
return openModal;
}
function setMenuProperties(rootMenu, subMenu) {
tabletRoot.rootMenu = rootMenu;
tabletRoot.subMenu = subMenu;

View file

@ -28,6 +28,7 @@
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation";
QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) {
@ -149,6 +150,15 @@ void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location
Setting::Handle<QVariant>(LAST_BROWSE_LOCATION_SETTING).set(location);
}
QString WindowScriptingInterface::getPreviousBrowseAssetLocation() const {
QString ASSETS_ROOT_PATH = "/";
return Setting::Handle<QString>(LAST_BROWSE_ASSETS_LOCATION_SETTING, ASSETS_ROOT_PATH).get();
}
void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& location) {
Setting::Handle<QVariant>(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location);
}
/// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and
/// might be in same thread as a script that sets the reticle to invisible
void WindowScriptingInterface::ensureReticleVisible() const {
@ -202,6 +212,31 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString&
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid
/// directory the browser will start at the root directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the asset browser at
/// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog`
/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue`
QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseAssetLocation();
}
if (path.left(1) != "/") {
path = "/" + path;
}
if (path.right(1) != "/") {
path = path + "/";
}
QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter);
if (!result.isEmpty()) {
setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath());
}
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
void WindowScriptingInterface::showAssetServer(const QString& upload) {
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
}

View file

@ -53,6 +53,7 @@ public slots:
CustomPromptResult customPrompt(const QVariant& config);
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void showAssetServer(const QString& upload = "");
void copyToClipboard(const QString& text);
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
@ -88,6 +89,9 @@ private:
QString getPreviousBrowseLocation() const;
void setPreviousBrowseLocation(const QString& location);
QString getPreviousBrowseAssetLocation() const;
void setPreviousBrowseAssetLocation(const QString& location);
void ensureReticleVisible() const;
int createMessageBox(QString title, QString text, int buttons, int defaultButton);

View file

@ -13,28 +13,28 @@
#include "UserActivityLogger.h"
void UserActivityLoggerScriptingInterface::enabledEdit() {
logAction("enabled_edit");
doLogAction("enabled_edit");
}
void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) {
logAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
doLogAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
}
void UserActivityLoggerScriptingInterface::closedTablet() {
logAction("closed_tablet");
doLogAction("closed_tablet");
}
void UserActivityLoggerScriptingInterface::openedMarketplace() {
logAction("opened_marketplace");
doLogAction("opened_marketplace");
}
void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) {
logAction("toggled_away", { { "is_away", isAway } });
doLogAction("toggled_away", { { "is_away", isAway } });
}
void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete,
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
logAction("tutorial_progress", {
doLogAction("tutorial_progress", {
{ "tutorial_run_id", tutorialRunID },
{ "tutorial_version", tutorialVersion },
{ "step", stepName },
@ -52,11 +52,11 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar
if (target.length() > 0) {
payload["target"] = target;
}
logAction("pal_activity", payload);
doLogAction("pal_activity", payload);
}
void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) {
logAction("pal_opened", {
doLogAction("pal_opened", {
{ "seconds_opened", secondsOpened }
});
}
@ -68,10 +68,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b
if (detailsString.length() > 0) {
payload["details"] = detailsString;
}
logAction("makeUserConnection", payload);
doLogAction("makeUserConnection", payload);
}
void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) {
void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) {
doLogAction(action, QJsonObject::fromVariantMap(details));
}
void UserActivityLoggerScriptingInterface::doLogAction(QString action, QJsonObject details) {
QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction",
Q_ARG(QString, action),
Q_ARG(QJsonObject, details));

View file

@ -29,9 +29,10 @@ public:
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
Q_INVOKABLE void palAction(QString action, QString target);
Q_INVOKABLE void palOpened(float secondsOpen);
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details="");
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = "");
Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{});
private:
void logAction(QString action, QJsonObject details = {});
void doLogAction(QString action, QJsonObject details = {});
};
#endif // hifi_UserActivityLoggerScriptingInterface_h

View file

@ -44,7 +44,8 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
if (callback.isFunction()) {
QScriptValueList args { };
QString error = request->getErrorString();
QScriptValueList args { error };
callback.call(_engine->currentContext()->thisObject(), args);
}
request->deleteLater();

View file

@ -72,6 +72,7 @@ public:
/**jsdoc
* Called when setMapping is complete
* @callback Assets~setMappingCallback
* @param {string} error
*/
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);

View file

@ -210,9 +210,11 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr
}
if (QThread::currentThread() != thread()) {
bool result;
QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, result),
Q_ARG(QScriptValue, getClipAtpUrl));
return false;
return result;
}
if (!_lastClip) {

View file

@ -43,6 +43,7 @@
#include <NetworkAccessManager.h>
#include <PathUtils.h>
#include <ResourceScriptingInterface.h>
#include <UserActivityLoggerScriptingInterface.h>
#include <NodeList.h>
#include <ScriptAvatarData.h>
#include <udt/PacketHeaders.h>
@ -678,6 +679,8 @@ void ScriptEngine::init() {
registerGlobalObject("Model", new ModelScriptingInterface(this));
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
}
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {

View file

@ -716,6 +716,86 @@ QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption,
return DependencyManager::get<OffscreenUi>()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options);
}
class AssetDialogListener : public ModalDialogListener {
// ATP equivalent of FileDialogListener.
Q_OBJECT
friend class OffscreenUi;
AssetDialogListener(QQuickItem* messageBox) : ModalDialogListener(messageBox) {
if (_finished) {
return;
}
connect(_dialog, SIGNAL(selectedAsset(QVariant)), this, SLOT(onSelectedAsset(QVariant)));
}
private slots:
void onSelectedAsset(QVariant asset) {
_result = asset;
_finished = true;
disconnect(_dialog);
}
};
QString OffscreenUi::assetDialog(const QVariantMap& properties) {
// ATP equivalent of fileDialog().
QVariant buildDialogResult;
bool invokeResult;
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
TabletProxy* tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"));
if (tablet->getToolbarMode()) {
invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog",
Q_RETURN_ARG(QVariant, buildDialogResult),
Q_ARG(QVariant, QVariant::fromValue(properties)));
} else {
QQuickItem* tabletRoot = tablet->getTabletRoot();
invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog",
Q_RETURN_ARG(QVariant, buildDialogResult),
Q_ARG(QVariant, QVariant::fromValue(properties)));
emit tabletScriptingInterface->tabletNotification();
}
if (!invokeResult) {
qWarning() << "Failed to create asset open dialog";
return QString();
}
QVariant result = AssetDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult();
if (!result.isValid()) {
return QString();
}
qCDebug(uiLogging) << result.toString();
return result.toUrl().toString();
}
QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
// ATP equivalent of fileOpenDialog().
if (QThread::currentThread() != thread()) {
QString result;
QMetaObject::invokeMethod(this, "assetOpenDialog", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QString, result),
Q_ARG(QString, caption),
Q_ARG(QString, dir),
Q_ARG(QString, filter),
Q_ARG(QString*, selectedFilter),
Q_ARG(QFileDialog::Options, options));
return result;
}
// FIXME support returning the selected filter... somehow?
QVariantMap map;
map.insert("caption", caption);
map.insert("dir", dir);
map.insert("filter", filter);
map.insert("options", static_cast<int>(options));
return assetDialog(map);
}
QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
// ATP equivalent of getOpenFileName().
return DependencyManager::get<OffscreenUi>()->assetOpenDialog(caption, dir, filter, selectedFilter, options);
}
bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) {
if (!filterEnabled(originalDestination, event)) {
return false;

View file

@ -118,6 +118,8 @@ public:
Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
// Compatibility with QFileDialog::getOpenFileName
static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
// Compatibility with QFileDialog::getSaveFileName
@ -125,6 +127,8 @@ public:
// Compatibility with QFileDialog::getExistingDirectory
static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant());
Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config);
QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current);
@ -155,6 +159,7 @@ signals:
private:
QString fileDialog(const QVariantMap& properties);
QString assetDialog(const QVariantMap& properties);
QQuickItem* _desktop { nullptr };
QQuickItem* _toolWindow { nullptr };

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,218 @@
/*
// record.css
//
// Created by David Rowe on 5 Apr 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
*/
body {
padding: 0;
overflow: hidden;
}
.title {
padding-left: 21px;
}
.title label {
font-size: 18px;
position: relative;
top: 12px;
}
#recordings {
height: 100%;
position: relative;
box-sizing: border-box;
padding: 51px 0 185px 0;
margin: 0 21px 0 21px;
}
#recordings #table-container {
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
border-left: 2px solid #575757;
border-right: 2px solid #575757;
background-color: #2e2e2e;
}
#recordings table {
border: none;
}
#recordings thead {
position: absolute;
top: 21px;
left: 0;
width: 100%;
box-sizing: border-box;
border: 2px solid #575757;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
border-bottom: 1px solid #575757;
position: absolute;
word-wrap: nowrap;
white-space: nowrap;
overflow: hidden;
}
#recordings table col#unload-column {
width: 100px;
}
#recordings thead th:last-child {
width: 100px;
}
#recordings table td {
text-overflow: ellipsis;
}
#recordings table td:nth-child(2) {
text-align: center;
}
#recordings tbody tr.filler td {
height: auto;
border-top: 1px solid #1c1c1c;
}
#recordings-list input {
height: 22px;
width: 22px;
min-width: 22px;
font-size: 16px;
padding: 0 1px 0 0;
}
#recordings tfoot {
position: absolute;
bottom: 159px;
left: 0;
width: 100%;
box-sizing: border-box;
border: 2px solid #575757;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
border-top: 1px solid #575757;
}
#recordings tfoot tr, #recordings tfoot td {
background: none;
}
#spinner {
text-align: center;
margin-top: 25%;
position: relative;
}
#spinner span {
display: block;
position: relative;
top: -101px;
color: #e2334d;
font-size: 60px;
font-weight: bold;
}
#recordings tfoot tr {
height: 24px;
}
#instructions td {
white-space: normal;
}
#instructions h1 {
font-size: 16px;
margin-top: 28px;
}
#instructions h1 + p {
margin-top: 14px;
}
#instructions p, #instructions ul {
margin-top: 21px;
font-size: 14px;
}
#instructions p {
font-family: Raleway-Bold;
}
#instructions ul {
font-family: Raleway-SemiBold;
margin-left: 21px;
font-weight: normal;
}
#instructions li {
margin-top: 7px;
}
#instructions ul input {
margin-left: 1px;
margin-top: 6px;
font-size: 14px;
padding: 0 7px;
}
#show-info-button {
font-family: HiFi-Glyphs;
font-size: 32px;
height: 16px;
line-height: 16px;
display: inline-block;
position: absolute;
top: 15px;
right: 5px;
margin-top: -11px;
margin-left: 7px;
}
#show-info-button:hover {
color: #00b4ef;
}
#record-controls {
position: absolute;
bottom: 7px;
width: 100%;
}
#record-controls #load-container {
position: absolute;
left: 21px;
}
#record-controls #record-container {
text-align: center;
}
#record-controls #checkbox-container {
margin-top: 31px;
}
#record-controls div.property {
padding-left: 21px;
}
.hidden {
display: none;
}

View file

@ -0,0 +1,298 @@
"use strict";
//
// record.js
//
// Created by David Rowe on 5 Apr 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var isUsingToolbar = false,
isDisplayingInstructions = false,
isRecording = false,
numberOfPlayers = 0,
recordingsBeingPlayed = [],
elRecordings,
elRecordingsTable,
elRecordingsList,
elInstructions,
elPlayersUnused,
elHideInfoButton,
elShowInfoButton,
elLoadButton,
elSpinner,
elCountdownNumber,
elRecordButton,
elFinishOnOpen,
elFinishOnOpenLabel,
EVENT_BRIDGE_TYPE = "record",
BODY_LOADED_ACTION = "bodyLoaded",
USING_TOOLBAR_ACTION = "usingToolbar",
RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed",
NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers",
STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording",
LOAD_RECORDING_ACTION = "loadRecording",
START_RECORDING_ACTION = "startRecording",
SET_COUNTDOWN_NUMBER_ACTION = "setCountdownNumber",
STOP_RECORDING_ACTION = "stopRecording",
FINISH_ON_OPEN_ACTION = "finishOnOpen";
function stopPlayingRecording(event) {
var playerID = event.target.getAttribute("playerID");
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: STOP_PLAYING_RECORDING_ACTION,
value: playerID
}));
}
function updatePlayersUnused() {
elPlayersUnused.innerHTML = numberOfPlayers - recordingsBeingPlayed.length;
}
function orderRecording(a, b) {
return a.filename > b.filename ? 1 : -1;
}
function updateRecordings() {
var tbody,
tr,
td,
input,
ths,
tds,
length,
i,
HIFI_GLYPH_CLOSE = "w";
recordingsBeingPlayed.sort(orderRecording);
tbody = document.createElement("tbody");
tbody.id = "recordings-list";
// <tr><td>Filename</td><td><input type="button" class="glyph red" value="w" playerID=id /></td></tr>
for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) {
tr = document.createElement("tr");
td = document.createElement("td");
td.innerHTML = recordingsBeingPlayed[i].filename.slice(4);
tr.appendChild(td);
td = document.createElement("td");
input = document.createElement("input");
input.setAttribute("type", "button");
input.setAttribute("class", "glyph red");
input.setAttribute("value", HIFI_GLYPH_CLOSE);
input.setAttribute("playerID", recordingsBeingPlayed[i].playerID);
input.addEventListener("click", stopPlayingRecording);
td.appendChild(input);
tr.appendChild(td);
tbody.appendChild(tr);
}
// Empty rows representing available players.
for (i = recordingsBeingPlayed.length, length = numberOfPlayers; i < length; i += 1) {
tr = document.createElement("tr");
td = document.createElement("td");
td.colSpan = 2;
tr.appendChild(td);
tbody.appendChild(tr);
}
// Filler row for extra table space.
tr = document.createElement("tr");
tr.classList.add("filler");
td = document.createElement("td");
td.colSpan = 2;
tr.appendChild(td);
tbody.appendChild(tr);
// Update table content.
elRecordingsTable.replaceChild(tbody, elRecordingsList);
elRecordingsList = document.getElementById("recordings-list");
// Update header cell widths to match content widths.
ths = document.querySelectorAll("#recordings-table thead th");
tds = document.querySelectorAll("#recordings-table tbody tr:first-child td");
for (i = 0; i < ths.length; i += 1) {
ths[i].width = tds[i].offsetWidth;
}
}
function updateInstructions() {
// Display show/hide instructions buttons if players are available.
if (numberOfPlayers === 0) {
elHideInfoButton.classList.add("hidden");
elShowInfoButton.classList.add("hidden");
} else {
elHideInfoButton.classList.remove("hidden");
elShowInfoButton.classList.remove("hidden");
}
// Display instructions if user requested or no players available.
if (isDisplayingInstructions || numberOfPlayers === 0) {
elRecordingsList.classList.add("hidden");
elInstructions.classList.remove("hidden");
} else {
elInstructions.classList.add("hidden");
elRecordingsList.classList.remove("hidden");
}
}
function showInstructions() {
isDisplayingInstructions = true;
updateInstructions();
}
function hideInstructions() {
isDisplayingInstructions = false;
updateInstructions();
}
function updateLoadButton() {
if (isRecording || numberOfPlayers <= recordingsBeingPlayed.length) {
elLoadButton.setAttribute("disabled", "disabled");
} else {
elLoadButton.removeAttribute("disabled");
}
}
function updateSpinner() {
if (isRecording) {
elRecordings.classList.add("hidden");
elSpinner.classList.remove("hidden");
} else {
elSpinner.classList.add("hidden");
elRecordings.classList.remove("hidden");
}
}
function updateFinishOnOpenLabel() {
var WINDOW_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen this window",
TABLET_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen tablet or window";
elFinishOnOpenLabel.innerHTML = isUsingToolbar ? WINDOW_FINISH_ON_OPEN_LABEL : TABLET_FINISH_ON_OPEN_LABEL;
}
function onScriptEventReceived(data) {
var message = JSON.parse(data);
if (message.type === EVENT_BRIDGE_TYPE) {
switch (message.action) {
case USING_TOOLBAR_ACTION:
isUsingToolbar = message.value;
updateFinishOnOpenLabel();
break;
case FINISH_ON_OPEN_ACTION:
elFinishOnOpen.checked = message.value;
break;
case START_RECORDING_ACTION:
isRecording = true;
elRecordButton.value = "Stop";
updateSpinner();
updateLoadButton();
break;
case SET_COUNTDOWN_NUMBER_ACTION:
elCountdownNumber.innerHTML = message.value;
break;
case STOP_RECORDING_ACTION:
isRecording = false;
elRecordButton.value = "Record";
updateSpinner();
updateLoadButton();
break;
case RECORDINGS_BEING_PLAYED_ACTION:
recordingsBeingPlayed = JSON.parse(message.value);
updateRecordings();
updatePlayersUnused();
updateInstructions();
updateLoadButton();
break;
case NUMBER_OF_PLAYERS_ACTION:
numberOfPlayers = message.value;
updateRecordings();
updatePlayersUnused();
updateInstructions();
updateLoadButton();
break;
}
}
}
function onLoadButtonClicked() {
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: LOAD_RECORDING_ACTION
}));
}
function onRecordButtonClicked() {
if (!isRecording) {
elRecordButton.value = "Stop";
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: START_RECORDING_ACTION
}));
isRecording = true;
updateSpinner();
updateLoadButton();
} else {
elRecordButton.value = "Record";
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: STOP_RECORDING_ACTION
}));
isRecording = false;
updateSpinner();
updateLoadButton();
}
}
function onFinishOnOpenClicked() {
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: FINISH_ON_OPEN_ACTION,
value: elFinishOnOpen.checked
}));
}
function signalBodyLoaded() {
EventBridge.emitWebEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: BODY_LOADED_ACTION
}));
}
function onBodyLoaded() {
EventBridge.scriptEventReceived.connect(onScriptEventReceived);
elRecordings = document.getElementById("recordings");
elRecordingsTable = document.getElementById("recordings-table");
elRecordingsList = document.getElementById("recordings-list");
elInstructions = document.getElementById("instructions");
elPlayersUnused = document.getElementById("players-unused");
elHideInfoButton = document.getElementById("hide-info-button");
elHideInfoButton.onclick = hideInstructions;
elShowInfoButton = document.getElementById("show-info-button");
elShowInfoButton.onclick = showInstructions;
elLoadButton = document.getElementById("load-button");
elLoadButton.onclick = onLoadButtonClicked;
elSpinner = document.getElementById("spinner");
elCountdownNumber = document.getElementById("countdown-number");
elRecordButton = document.getElementById("record-button");
elRecordButton.onclick = onRecordButtonClicked;
elFinishOnOpen = document.getElementById("finish-on-open");
elFinishOnOpen.onclick = onFinishOnOpenClicked;
elFinishOnOpenLabel = document.getElementById("finish-on-open-label");
signalBodyLoaded();
}

View file

@ -0,0 +1,87 @@
<!--
// record.html
//
// Created by David Rowe on 5 Apr 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-->
<html>
<head>
<title>Record</title>
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
<link rel="stylesheet" type="text/css" href="css/record.css">
<script type="text/javascript" src="js/record.js"></script>
</head>
<body>
<div class="title">
<label>Record</label>
</div>
<hr />
<div id="recordings">
<div id="table-container">
<table id="recordings-table">
<colgroup>
<col />
<col id="unload-column" />
</colgroup>
<thead>
<tr>
<th>Recordings Being Played</th>
<th>Unload</th>
</tr>
</thead>
<tbody id="recordings-list"></tbody>
<tbody id="instructions" class="hidden">
<tr>
<td colspan="2">
<p>This app lets you record and play back multiple instances of your avatar in your Sandbox.</p>
<h1>Setup Instructions</h1>
<p>In your sandbox domain:</p>
<ul>
<li>Right-click the High Fidelity Sandbox icon in your system tray and click &ldquo;Settings&rdquo;.</li>
<li>In the &ldquo;Scripts&rdquo; section add a new row and paste in this script URL:<br />
<input type="text" value="https://content.highfidelity.com/Scripts/playRecordingAC.js" readonly />
</li>
<li>Set the number of recordings you&rsquo;d like to run at a given time in the &ldquo;Instances&rdquo; slot.</li>
<li>Click &ldquo;Save and restart&rdquo;.</li>
</ul>
<p>Now you can record and play back recordings in your domain!</p>
<p><input id="hide-info-button" type="button" class="blue" value="Got It" /></p>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td id="footer-text" colspan="2">
Number of available instances: <span id="players-unused"></span>
<span id="show-info-button">[</span>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div id="spinner" class="hidden">
<img src="../../../resources/icons/loader-red-countdown-ring.gif" />
<span id="countdown-number">3</span>
</div>
<div id="record-controls">
<div id="load-container">
<input id="load-button" type="button" value="Load" disabled />
</div>
<div id="record-container">
<input id="record-button" class="red" type="button" value="Record" />
</div>
<div id="checkbox-container" class="property checkbox">
<input type="checkbox" id="finish-on-open">
<label for="finish-on-open" id="finish-on-open-label">Stop recording automatically when ...</label>
</div>
</div>
<script>
onBodyLoaded();
</script>
</body>
</html>

View file

@ -0,0 +1,439 @@
"use strict";
//
// playRecordingAC.js
//
// Created by David Rowe on 7 Apr 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function () {
var APP_NAME = "PLAYBACK",
HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel",
HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel",
PLAYER_COMMAND_PLAY = "play",
PLAYER_COMMAND_STOP = "stop",
heartbeatTimer = null,
HEARTBEAT_INTERVAL = 3000,
TIMESTAMP_UPDATE_INTERVAL = 2500,
AUTOPLAY_SEARCH_INTERVAL = 5000,
AUTOPLAY_ERROR_INTERVAL = 30000, // 30s
scriptUUID,
Entity,
Player;
function log(message) {
print(APP_NAME + " " + scriptUUID + ": " + message);
}
Entity = (function () {
// Persistence of playback via invisible entity.
var entityID = null,
userData,
updateTimestampTimer = null,
ENTITY_NAME = "Recording",
ENTITY_DESCRIPTION = "Avatar recording to play back",
ENTITIY_POSITION = { x: -16382, y: -16382, z: -16382 }, // Near but not right on domain corner.
ENTITY_SEARCH_DELTA = { x: 1, y: 1, z: 1 }, // Allow for position imprecision.
SEARCH_IDLE = 0,
SEARCH_SEARCHING = 1,
SEARCH_CLAIMING = 2,
SEARCH_PAUSING = 3,
searchState = SEARCH_IDLE,
otherPlayersPlaying,
otherPlayersPlayingCounts,
pauseCount;
function onUpdateTimestamp() {
userData.timestamp = Date.now();
Entities.editEntity(entityID, { userData: JSON.stringify(userData) });
EntityViewer.queryOctree(); // Keep up to date ready for find().
}
function id() {
return entityID;
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function onMessageReceived(channel, message, sender) {
var index;
if (sender !== scriptUUID) {
message = JSON.parse(message);
index = otherPlayersPlaying.indexOf(message.entity);
if (index !== -1) {
otherPlayersPlayingCounts[index] += 1;
} else {
otherPlayersPlaying.push(message.entity);
otherPlayersPlayingCounts.push(1);
}
}
}
function create(filename, position, orientation) {
// Create a new persistence entity (even if already have one but that should never occur).
var properties;
log("Create recording " + filename);
if (updateTimestampTimer !== null) {
Script.clearInterval(updateTimestampTimer); // Just in case.
}
searchState = SEARCH_IDLE;
userData = {
recording: filename,
position: position,
orientation: orientation,
scriptUUID: scriptUUID,
timestamp: Date.now()
};
properties = {
type: "Box",
name: ENTITY_NAME,
description: ENTITY_DESCRIPTION,
position: ENTITIY_POSITION,
visible: false,
userData: JSON.stringify(userData)
};
entityID = Entities.addEntity(properties);
if (!Uuid.isNull(entityID)) {
updateTimestampTimer = Script.setInterval(onUpdateTimestamp, TIMESTAMP_UPDATE_INTERVAL);
return true;
}
return false;
}
function find() {
// Find a persistence entity that isn't being played.
// AC scripts may simultaneously find the same entity to play because octree updates aren't instantaneously
// propagated. Additionally, messages are not instantaneous. To address these issues the "find" progresses through
// the following search states:
// - SEARCH_IDLE
// No searching is being performed.
// Return null.
// - SEARCH_SEARCHING
// Looking for an entity that isn't being played (as reported in entity properties) and isn't being claimed (as
// reported by heartbeat messages. If one is found transition to SEARCH_CLAIMING and start reporting the entity
// in heartbeat messages.
// Return null.
// - SEARCH_CLAIMING
// An entity has been found and is reported in heartbeat messages but isn't being played yet. After a period of
// time, if no other players report they're playing that entity then transition to SEARCH_IDLE otherwise
// transition to SEARCH_PAUSING.
// If transitioning to SEARCH_IDLE update the entity userData and return the recording details, otherwise
// return null;
// - SEARCH_PAUSING
// Two or more players have tried to play the same entity. Wait for a randomized period of time before
// transitioning to SEARCH_SEARCHING.
// Return null.
// One of these states is processed each find() call.
var entityIDs,
index,
found = false,
properties,
numberOfClaims,
result = null;
switch (searchState) {
case SEARCH_IDLE:
log("Start searching");
otherPlayersPlaying = [];
otherPlayersPlayingCounts = [];
Messages.subscribe(HIFI_RECORDER_CHANNEL);
Messages.messageReceived.connect(onMessageReceived);
searchState = SEARCH_SEARCHING;
break;
case SEARCH_SEARCHING:
// Find an entity that isn't being played or claimed.
entityIDs = Entities.findEntities(ENTITIY_POSITION, ENTITY_SEARCH_DELTA.x);
if (entityIDs.length > 0) {
index = -1;
while (!found && index < entityIDs.length - 1) {
index += 1;
if (otherPlayersPlaying.indexOf(entityIDs[index]) === -1) {
properties = Entities.getEntityProperties(entityIDs[index], ["name", "userData"]);
userData = JSON.parse(properties.userData);
found = properties.name === ENTITY_NAME && userData.recording !== undefined;
}
}
}
// Claim entity if found.
if (found) {
log("Claim entity " + entityIDs[index]);
entityID = entityIDs[index];
searchState = SEARCH_CLAIMING;
}
break;
case SEARCH_CLAIMING:
// How many other players are claiming (or playing) this entity?
index = otherPlayersPlaying.indexOf(entityID);
numberOfClaims = index !== -1 ? otherPlayersPlayingCounts[index] : 0;
// Have found an entity to play if no other players are also claiming it.
if (numberOfClaims === 0) {
log("Complete claim " + entityID);
Messages.messageReceived.disconnect(onMessageReceived);
Messages.unsubscribe(HIFI_RECORDER_CHANNEL);
searchState = SEARCH_IDLE;
userData.scriptUUID = scriptUUID;
userData.timestamp = Date.now();
Entities.editEntity(entityID, { userData: JSON.stringify(userData) });
updateTimestampTimer = Script.setInterval(onUpdateTimestamp, TIMESTAMP_UPDATE_INTERVAL);
result = { recording: userData.recording, position: userData.position, orientation: userData.orientation };
break;
}
// Otherwise back off for a bit before resuming search.
log("Release claim " + entityID + " and pause searching");
entityID = null;
pauseCount = randomInt(0, otherPlayersPlaying.length);
searchState = SEARCH_PAUSING;
break;
case SEARCH_PAUSING:
// Resume searching if have paused long enough.
pauseCount -= 1;
if (pauseCount < 0) {
log("Resume searching");
otherPlayersPlaying = [];
otherPlayersPlayingCounts = [];
searchState = SEARCH_SEARCHING;
}
break;
}
EntityViewer.queryOctree();
return result;
}
function destroy() {
// Delete current persistence entity.
if (entityID !== null) { // Just in case.
Entities.deleteEntity(entityID);
entityID = null;
searchState = SEARCH_IDLE;
}
if (updateTimestampTimer !== null) { // Just in case.
Script.clearInterval(updateTimestampTimer);
}
}
function setUp() {
// Set up EntityViewer so that can do Entities.findEntities().
// Position and orientation set so that viewing entities only in corner of domain.
var entityViewerPosition = Vec3.sum(ENTITIY_POSITION, ENTITY_SEARCH_DELTA);
EntityViewer.setPosition(entityViewerPosition);
EntityViewer.setOrientation(Quat.lookAtSimple(entityViewerPosition, ENTITIY_POSITION));
EntityViewer.queryOctree();
}
function tearDown() {
// Nothing to do.
}
return {
id: id,
create: create,
find: find,
destroy: destroy,
setUp: setUp,
tearDown: tearDown
};
}());
Player = (function () {
// Recording playback functions.
var isPlayingRecording = false,
recordingFilename = "",
autoPlayTimer = null,
playRecording;
function play(recording, position, orientation) {
if (Entity.create(recording, position, orientation)) {
log("Play new recording " + recordingFilename);
isPlayingRecording = true;
recordingFilename = recording;
playRecording(recordingFilename, position, orientation);
} else {
log("Could not create entity to play new recording " + recordingFilename);
}
}
function autoPlay() {
var recording,
AUTOPLAY_SEARCH_DELTA = 1000;
// Random delay to help reduce collisions between AC scripts.
Script.setTimeout(function () {
recording = Entity.find();
if (recording) {
log("Play persisted recording " + recordingFilename);
playRecording(recording.recording, recording.position, recording.orientation);
} else {
autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_SEARCH_INTERVAL); // Try again soon.
}
}, Math.random() * AUTOPLAY_SEARCH_DELTA);
}
playRecording = function (recording, position, orientation) {
Recording.loadRecording(recording, function (success) {
if (success) {
Users.disableIgnoreRadius();
Agent.isAvatar = true;
Avatar.position = position;
Avatar.orientation = orientation;
Recording.setPlayFromCurrentLocation(true);
Recording.setPlayerUseDisplayName(true);
Recording.setPlayerUseHeadModel(false);
Recording.setPlayerUseAttachments(true);
Recording.setPlayerLoop(true);
Recording.setPlayerUseSkeletonModel(true);
isPlayingRecording = true;
recordingFilename = recording;
Recording.setPlayerTime(0.0);
Recording.startPlaying();
UserActivityLogger.logAction("playRecordingAC_play_recording");
} else {
log("Failed to load recording " + recording);
autoPlayTimer = Script.setTimeout(autoPlay, AUTOPLAY_ERROR_INTERVAL); // Try again later.
}
});
};
function stop() {
log("Stop playing " + recordingFilename);
Entity.destroy();
if (Recording.isPlaying()) {
Recording.stopPlaying();
Agent.isAvatar = false;
}
isPlayingRecording = false;
recordingFilename = "";
}
function isPlaying() {
return isPlayingRecording;
}
function recording() {
return recordingFilename;
}
function setUp() {
Entity.setUp();
}
function tearDown() {
if (autoPlayTimer) {
Script.clearTimeout(autoPlayTimer);
autoPlayTimer = null;
}
Entity.tearDown();
}
return {
autoPlay: autoPlay,
play: play,
stop: stop,
isPlaying: isPlaying,
recording: recording,
setUp: setUp,
tearDown: tearDown
};
}());
function sendHeartbeat() {
Messages.sendMessage(HIFI_RECORDER_CHANNEL, JSON.stringify({
playing: Player.isPlaying(),
recording: Player.recording(),
entity: Entity.id()
}));
heartbeatTimer = Script.setTimeout(sendHeartbeat, HEARTBEAT_INTERVAL);
}
function stopHeartbeat() {
if (heartbeatTimer) {
Script.clearTimeout(heartbeatTimer);
heartbeatTimer = null;
}
}
function onMessageReceived(channel, message, sender) {
if (channel !== HIFI_PLAYER_CHANNEL) {
return;
}
message = JSON.parse(message);
if (message.player === scriptUUID) {
switch (message.command) {
case PLAYER_COMMAND_PLAY:
if (!Player.isPlaying()) {
Player.play(message.recording, message.position, message.orientation);
} else {
log("Didn't start playing " + message.recording + " because already playing " + Player.recording());
}
sendHeartbeat();
break;
case PLAYER_COMMAND_STOP:
Player.stop();
Player.autoPlay(); // There may be another recording to play.
sendHeartbeat();
break;
}
}
}
function setUp() {
scriptUUID = Agent.sessionUUID;
Player.setUp();
Messages.messageReceived.connect(onMessageReceived);
Messages.subscribe(HIFI_PLAYER_CHANNEL);
Player.autoPlay();
sendHeartbeat();
UserActivityLogger.logAction("playRecordingAC_script_load");
}
function tearDown() {
stopHeartbeat();
Player.stop();
Messages.messageReceived.disconnect(onMessageReceived);
Messages.unsubscribe(HIFI_PLAYER_CHANNEL);
Player.tearDown();
}
setUp();
Script.scriptEnding.connect(tearDown);
}());

694
scripts/system/record.js Normal file
View file

@ -0,0 +1,694 @@
"use strict";
//
// record.js
//
// Created by David Rowe on 5 Apr 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function () {
var APP_NAME = "RECORD",
APP_ICON_INACTIVE = "icons/tablet-icons/avatar-record-i.svg",
APP_ICON_ACTIVE = "icons/tablet-icons/avatar-record-a.svg",
APP_URL = Script.resolvePath("html/record.html"),
isDialogDisplayed = false,
tablet,
button,
isConnected,
RecordingIndicator,
Recorder,
Player,
Dialog,
SCRIPT_STARTUP_DELAY = 3000; // 3s
function log(message) {
print(APP_NAME + ": " + message);
}
function error(message, info) {
print(APP_NAME + ": " + message + (info !== undefined ? " - " + info : ""));
Window.alert(message);
}
RecordingIndicator = (function () {
// Displays "recording" overlay.
var hmdOverlay,
HMD_FONT_SIZE = 0.08,
desktopOverlay,
DESKTOP_FONT_SIZE = 24;
function show() {
// Create both overlays in case user switches desktop/HMD mode.
var screenSize = Controller.getViewportDimensions(),
recordingText = "REC", // Unicode circle \u25cf doesn't render in HMD.
CAMERA_JOINT_INDEX = -7;
if (HMD.active) {
// 3D overlay attached to avatar.
hmdOverlay = Overlays.addOverlay("text3d", {
text: recordingText,
dimensions: { x: 3 * HMD_FONT_SIZE, y: HMD_FONT_SIZE },
parentID: MyAvatar.sessionUUID,
parentJointIndex: CAMERA_JOINT_INDEX,
localPosition: { x: 0.95, y: 0.95, z: -2.0 },
color: { red: 255, green: 0, blue: 0 },
alpha: 0.9,
lineHeight: HMD_FONT_SIZE,
backgroundAlpha: 0,
ignoreRayIntersection: true,
isFacingAvatar: true,
drawInFront: true,
visible: true
});
} else {
// 2D overlay on desktop.
desktopOverlay = Overlays.addOverlay("text", {
text: recordingText,
width: 3 * DESKTOP_FONT_SIZE,
height: DESKTOP_FONT_SIZE,
x: screenSize.x - 4 * DESKTOP_FONT_SIZE,
y: DESKTOP_FONT_SIZE,
font: { size: DESKTOP_FONT_SIZE },
color: { red: 255, green: 8, blue: 8 },
alpha: 1.0,
backgroundAlpha: 0,
visible: true
});
}
}
function hide() {
if (desktopOverlay) {
Overlays.deleteOverlay(desktopOverlay);
}
if (hmdOverlay) {
Overlays.deleteOverlay(hmdOverlay);
}
}
return {
show: show,
hide: hide
};
}());
Recorder = (function () {
// Makes the recording and uploads it to the domain's Asset Server.
var IDLE = 0,
COUNTING_DOWN = 1,
RECORDING = 2,
recordingState = IDLE,
mappingPath,
startPosition,
startOrientation,
play,
countdownTimer,
countdownSeconds,
COUNTDOWN_SECONDS = 3,
tickSound,
startRecordingSound,
finishRecordingSound,
TICK_SOUND = "assets/sounds/countdown-tick.wav",
START_RECORDING_SOUND = "assets/sounds/start-recording.wav",
FINISH_RECORDING_SOUND = "assets/sounds/finish-recording.wav",
START_RECORDING_SOUND_DURATION = 1200,
SOUND_VOLUME = 0.2;
function playSound(sound) {
Audio.playSound(sound, {
position: MyAvatar.position,
localOnly: true,
volume: SOUND_VOLUME
});
}
function setMappingCallback(status) {
if (status !== "") {
error("Error mapping recording to " + mappingPath + " on Asset Server!", status);
return;
}
log("Recording mapped to " + mappingPath);
log("Request play recording");
play("atp:" + mappingPath, startPosition, startOrientation);
}
function saveRecordingToAssetCallback(url) {
var filename,
hash;
if (url === "") {
error("Error saving recording to Asset Server!");
return;
}
log("Recording saved to Asset Server as " + url);
filename = (new Date()).toISOString(); // yyyy-mm-ddThh:mm:ss.sssZ
filename = filename.replace(/[\-:]|\.\d*Z$/g, "").replace("T", "-") + ".hfr"; // yyyymmmdd-hhmmss.hfr
hash = url.slice(4); // Remove leading "atp:" from url.
mappingPath = "/recordings/" + filename;
Assets.setMapping(mappingPath, hash, setMappingCallback);
}
function startRecording() {
recordingState = RECORDING;
log("Start recording");
playSound(startRecordingSound);
Script.setTimeout(function () {
// Delay start so that start beep is not included in recorded sound.
startPosition = MyAvatar.position;
startOrientation = MyAvatar.orientation;
Recording.startRecording();
RecordingIndicator.show();
}, START_RECORDING_SOUND_DURATION);
}
function finishRecording() {
var success,
error;
recordingState = IDLE;
log("Finish recording");
UserActivityLogger.logAction("record_finish_recording");
playSound(finishRecordingSound);
Recording.stopRecording();
RecordingIndicator.hide();
success = Recording.saveRecordingToAsset(saveRecordingToAssetCallback);
if (!success) {
error("Error saving recording to Asset Server!");
}
}
function cancelRecording() {
Recording.stopRecording();
RecordingIndicator.hide();
recordingState = IDLE;
log("Cancel recording");
}
function finishCountdown() {
Dialog.setCountdownNumber("");
recordingState = RECORDING;
startRecording();
}
function cancelCountdown() {
recordingState = IDLE;
Script.clearInterval(countdownTimer);
Dialog.setCountdownNumber("");
log("Cancel countdown");
}
function startCountdown() {
recordingState = COUNTING_DOWN;
log("Start countdown");
countdownSeconds = COUNTDOWN_SECONDS;
Dialog.setCountdownNumber(countdownSeconds);
playSound(tickSound);
countdownTimer = Script.setInterval(function () {
countdownSeconds -= 1;
if (countdownSeconds <= 0) {
Script.clearInterval(countdownTimer);
finishCountdown();
} else {
Dialog.setCountdownNumber(countdownSeconds);
playSound(tickSound);
}
}, 1000);
}
function isIdle() {
return recordingState === IDLE;
}
function isCountingDown() {
return recordingState === COUNTING_DOWN;
}
function isRecording() {
return recordingState === RECORDING;
}
function setUp(playerCallback) {
play = playerCallback;
tickSound = SoundCache.getSound(Script.resolvePath(TICK_SOUND));
startRecordingSound = SoundCache.getSound(Script.resolvePath(START_RECORDING_SOUND));
finishRecordingSound = SoundCache.getSound(Script.resolvePath(FINISH_RECORDING_SOUND));
}
function tearDown() {
// Nothing to do; any cancelling of recording needs to be done by script using this object.
}
return {
startCountdown: startCountdown,
cancelCountdown: cancelCountdown,
startRecording: startRecording,
cancelRecording: cancelRecording,
finishRecording: finishRecording,
isIdle: isIdle,
isCountingDown: isCountingDown,
isRecording: isRecording,
setUp: setUp,
tearDown: tearDown
};
}());
Player = (function () {
var HIFI_RECORDER_CHANNEL = "HiFi-Recorder-Channel",
HIFI_PLAYER_CHANNEL = "HiFi-Player-Channel",
PLAYER_COMMAND_PLAY = "play",
PLAYER_COMMAND_STOP = "stop",
playerIDs = [], // UUIDs of AC player scripts.
playerIsPlayings = [], // True if AC player script is playing a recording.
playerRecordings = [], // Assignment client mappings of recordings being played.
playerTimestamps = [], // Timestamps of last heartbeat update from player script.
updateTimer,
UPDATE_INTERVAL = 5000; // Must be > player's HEARTBEAT_INTERVAL.
function numberOfPlayers() {
return playerIDs.length;
}
function updatePlayers() {
var now = Date.now(),
countBefore = playerIDs.length,
i;
// Remove players that haven't sent a heartbeat for a while.
for (i = playerTimestamps.length - 1; i >= 0; i -= 1) {
if (now - playerTimestamps[i] > UPDATE_INTERVAL) {
playerIDs.splice(i, 1);
playerIsPlayings.splice(i, 1);
playerRecordings.splice(i, 1);
playerTimestamps.splice(i, 1);
}
}
// Update UI.
if (playerIDs.length !== countBefore) {
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
}
function playRecording(recording, position, orientation) {
var index,
CHECK_PLAYING_TIMEOUT = 10000;
// Optional function parameters.
if (position === undefined) {
position = MyAvatar.position;
}
if (orientation === undefined) {
orientation = MyAvatar.orientation;
}
index = playerIsPlayings.indexOf(false);
if (index === -1) {
error("No player instance available to play recording "
+ recording.slice(4) + "!"); // Remove leading "atp:" from recording.
return;
}
Messages.sendMessage(HIFI_PLAYER_CHANNEL, JSON.stringify({
player: playerIDs[index],
command: PLAYER_COMMAND_PLAY,
recording: recording,
position: position,
orientation: orientation
}));
Script.setTimeout(function () {
if (!playerIsPlayings[index] || playerRecordings[index] !== recording) {
error("Didn't start playing recording "
+ recording.slice(4) + "!"); // Remove leading "atp:" from recording.
}
}, CHECK_PLAYING_TIMEOUT);
}
function stopPlayingRecording(playerID) {
Messages.sendMessage(HIFI_PLAYER_CHANNEL, JSON.stringify({
player: playerID,
command: PLAYER_COMMAND_STOP
}));
}
function onMessageReceived(channel, message, sender) {
// Heartbeat from AC script.
var index;
if (channel !== HIFI_RECORDER_CHANNEL) {
return;
}
message = JSON.parse(message);
index = playerIDs.indexOf(sender);
if (index === -1) {
index = playerIDs.length;
playerIDs[index] = sender;
}
playerIsPlayings[index] = message.playing;
playerRecordings[index] = message.recording;
playerTimestamps[index] = Date.now();
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
function reset() {
playerIDs = [];
playerIsPlayings = [];
playerRecordings = [];
playerTimestamps = [];
Dialog.updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs);
}
function setUp() {
// Messaging with AC scripts.
Messages.messageReceived.connect(onMessageReceived);
Messages.subscribe(HIFI_RECORDER_CHANNEL);
updateTimer = Script.setInterval(updatePlayers, UPDATE_INTERVAL);
}
function tearDown() {
Script.clearInterval(updateTimer);
Messages.messageReceived.disconnect(onMessageReceived);
Messages.subscribe(HIFI_RECORDER_CHANNEL);
}
return {
playRecording: playRecording,
stopPlayingRecording: stopPlayingRecording,
numberOfPlayers: numberOfPlayers,
reset: reset,
setUp: setUp,
tearDown: tearDown
};
}());
Dialog = (function () {
var isFinishOnOpen = false,
countdownNumber = "",
EVENT_BRIDGE_TYPE = "record",
BODY_LOADED_ACTION = "bodyLoaded",
USING_TOOLBAR_ACTION = "usingToolbar",
RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed",
NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers",
STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording",
LOAD_RECORDING_ACTION = "loadRecording",
START_RECORDING_ACTION = "startRecording",
SET_COUNTDOWN_NUMBER_ACTION = "setCountdownNumber",
STOP_RECORDING_ACTION = "stopRecording",
FINISH_ON_OPEN_ACTION = "finishOnOpen",
SETTINGS_FINISH_ON_OPEN = "record/finishOnOpen";
function isUsingToolbar() {
return ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar"))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar")));
}
function updateRecordingStatus(isRecording) {
if (isRecording) {
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: START_RECORDING_ACTION
}));
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: SET_COUNTDOWN_NUMBER_ACTION,
value: countdownNumber
}));
} else {
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: STOP_RECORDING_ACTION
}));
}
}
function updatePlayerDetails(playerIsPlayings, playerRecordings, playerIDs) {
var recordingsBeingPlayed = [],
length,
i;
for (i = 0, length = playerIsPlayings.length; i < length; i += 1) {
if (playerIsPlayings[i]) {
recordingsBeingPlayed.push({
filename: playerRecordings[i],
playerID: playerIDs[i]
});
}
}
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: RECORDINGS_BEING_PLAYED_ACTION,
value: JSON.stringify(recordingsBeingPlayed)
}));
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: NUMBER_OF_PLAYERS_ACTION,
value: playerIsPlayings.length
}));
}
function setCountdownNumber(number) {
countdownNumber = number;
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: SET_COUNTDOWN_NUMBER_ACTION,
value: countdownNumber
}));
}
function finishOnOpen() {
return isFinishOnOpen;
}
function onWebEventReceived(data) {
var message,
recording;
message = JSON.parse(data);
if (message.type === EVENT_BRIDGE_TYPE) {
switch (message.action) {
case BODY_LOADED_ACTION:
// Dialog's ready; initialize its state.
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: USING_TOOLBAR_ACTION,
value: isUsingToolbar()
}));
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: FINISH_ON_OPEN_ACTION,
value: isFinishOnOpen
}));
tablet.emitScriptEvent(JSON.stringify({
type: EVENT_BRIDGE_TYPE,
action: NUMBER_OF_PLAYERS_ACTION,
value: Player.numberOfPlayers()
}));
updateRecordingStatus(!Recorder.isIdle());
UserActivityLogger.logAction("record_open_dialog");
break;
case STOP_PLAYING_RECORDING_ACTION:
// Stop the specified player.
Player.stopPlayingRecording(message.value);
break;
case LOAD_RECORDING_ACTION:
// User wants to select an ATP recording to play.
recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hfr");
if (recording) {
log("Load recording " + recording);
UserActivityLogger.logAction("record_load_recording");
Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation);
}
break;
case START_RECORDING_ACTION:
// Start making a recording.
if (Recorder.isIdle()) {
Recorder.startCountdown();
}
break;
case STOP_RECORDING_ACTION:
// Cancel or finish a recording.
if (Recorder.isCountingDown()) {
Recorder.cancelCountdown();
} else if (Recorder.isRecording()) {
Recorder.finishRecording();
}
break;
case FINISH_ON_OPEN_ACTION:
// Set behavior on dialog open.
isFinishOnOpen = message.value;
Settings.setValue(SETTINGS_FINISH_ON_OPEN, isFinishOnOpen);
break;
}
}
}
function setUp() {
isFinishOnOpen = Settings.getValue(SETTINGS_FINISH_ON_OPEN) === true;
tablet.webEventReceived.connect(onWebEventReceived);
}
function tearDown() {
tablet.webEventReceived.disconnect(onWebEventReceived);
}
return {
updatePlayerDetails: updatePlayerDetails,
updateRecordingStatus: updateRecordingStatus,
setCountdownNumber: setCountdownNumber,
finishOnOpen: finishOnOpen,
setUp: setUp,
tearDown: tearDown
};
}());
function onTabletScreenChanged(type, url) {
// Opened/closed dialog in tablet or window.
var RECORD_URL = "/scripts/system/html/record.html";
if (type === "Web" && url.slice(-RECORD_URL.length) === RECORD_URL) {
if (Dialog.finishOnOpen()) {
// Cancel countdown or finish recording.
if (Recorder.isCountingDown()) {
Recorder.cancelCountdown();
} else if (Recorder.isRecording()) {
Recorder.finishRecording();
}
Dialog.updateRecordingStatus(false);
}
isDialogDisplayed = true;
} else {
isDialogDisplayed = false;
}
button.editProperties({ isActive: isDialogDisplayed });
}
function onTabletShownChanged() {
// Opened/closed tablet.
if (tablet.tabletShown && Dialog.finishOnOpen()) {
// Cancel countdown or finish recording.
if (Recorder.isCountingDown()) {
Recorder.cancelCountdown();
} else if (Recorder.isRecording()) {
Recorder.finishRecording();
}
Dialog.updateRecordingStatus(false);
}
}
function onButtonClicked() {
if (isDialogDisplayed) {
// Can click icon in toolbar mode; gotoHomeScreen() closes dialog.
tablet.gotoHomeScreen();
isDialogDisplayed = false;
} else {
tablet.gotoWebScreen(APP_URL);
isDialogDisplayed = true;
}
}
function onUpdate() {
if (isConnected !== Window.location.isConnected) {
// Server restarted or domain changed.
isConnected = !isConnected;
if (!isConnected) {
// Clear dialog.
Player.reset();
}
}
}
function setUp() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
if (!tablet) {
return;
}
// Tablet/toolbar button.
button = tablet.addButton({
icon: APP_ICON_INACTIVE,
activeIcon: APP_ICON_ACTIVE,
text: APP_NAME,
isActive: false
});
if (button) {
button.clicked.connect(onButtonClicked);
}
// Track showing/hiding tablet/dialog.
tablet.screenChanged.connect(onTabletScreenChanged);
tablet.tabletShownChanged.connect(onTabletShownChanged);
Dialog.setUp();
Player.setUp();
Recorder.setUp(Player.playRecording);
isConnected = Window.location.isConnected;
Script.update.connect(onUpdate);
UserActivityLogger.logAction("record_run_script");
}
function tearDown() {
if (!tablet) {
return;
}
Script.update.disconnect(onUpdate);
Recorder.tearDown();
Player.tearDown();
Dialog.tearDown();
tablet.tabletShownChanged.disconnect(onTabletShownChanged);
tablet.screenChanged.disconnect(onTabletScreenChanged);
if (button) {
button.clicked.disconnect(onButtonClicked);
tablet.removeButton(button);
button = null;
}
if (Recorder.isCountingDown()) {
Recorder.cancelCountdown();
} else if (Recorder.isRecording()) {
Recorder.cancelRecording();
}
if (isDialogDisplayed) {
tablet.gotoHomeScreen();
}
tablet = null;
}
// FIXME: If setUp() is run immediately at Interface start-up, Interface hangs and crashes because of the line of code:
// tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
//setUp();
//Script.scriptEnding.connect(tearDown);
Script.setTimeout(function () {
setUp();
Script.scriptEnding.connect(tearDown);
}, SCRIPT_STARTUP_DELAY);
}());