mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 19:29:47 +02:00
Merge pull request #10130 from ctrlaltdavid/21249
New avatar record and playback script
This commit is contained in:
commit
5bd9b2e631
27 changed files with 2691 additions and 14 deletions
|
@ -30,6 +30,7 @@
|
||||||
#include <ShutdownEventListener.h>
|
#include <ShutdownEventListener.h>
|
||||||
#include <SoundCache.h>
|
#include <SoundCache.h>
|
||||||
#include <ResourceScriptingInterface.h>
|
#include <ResourceScriptingInterface.h>
|
||||||
|
#include <UserActivityLoggerScriptingInterface.h>
|
||||||
|
|
||||||
#include "AssignmentFactory.h"
|
#include "AssignmentFactory.h"
|
||||||
#include "AssignmentDynamicFactory.h"
|
#include "AssignmentDynamicFactory.h"
|
||||||
|
@ -66,6 +67,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
||||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||||
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
|
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
|
||||||
DependencyManager::set<ResourceScriptingInterface>();
|
DependencyManager::set<ResourceScriptingInterface>();
|
||||||
|
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||||
|
|
||||||
// setup a thread for the NodeList and its PacketReceiver
|
// setup a thread for the NodeList and its PacketReceiver
|
||||||
QThread* nodeThread = new QThread(this);
|
QThread* nodeThread = new QThread(this);
|
||||||
|
|
BIN
interface/resources/icons/loader-red-countdown-ring.gif
Normal file
BIN
interface/resources/icons/loader-red-countdown-ring.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
109
interface/resources/icons/tablet-icons/avatar-record-a.svg
Normal file
109
interface/resources/icons/tablet-icons/avatar-record-a.svg
Normal 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 |
36
interface/resources/icons/tablet-icons/avatar-record-i.svg
Normal file
36
interface/resources/icons/tablet-icons/avatar-record-i.svg
Normal 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 |
|
@ -466,6 +466,11 @@ FocusScope {
|
||||||
return fileDialogBuilder.createObject(desktop, properties);
|
return fileDialogBuilder.createObject(desktop, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component { id: assetDialogBuilder; AssetDialog { } }
|
||||||
|
function assetDialog(properties) {
|
||||||
|
return assetDialogBuilder.createObject(desktop, properties);
|
||||||
|
}
|
||||||
|
|
||||||
function unfocusWindows() {
|
function unfocusWindows() {
|
||||||
// First find the active focus item, and unfocus it, all the way
|
// First find the active focus item, and unfocus it, all the way
|
||||||
// up the parent chain to the window
|
// up the parent chain to the window
|
||||||
|
|
58
interface/resources/qml/dialogs/AssetDialog.qml
Normal file
58
interface/resources/qml/dialogs/AssetDialog.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
53
interface/resources/qml/dialogs/TabletAssetDialog.qml
Normal file
53
interface/resources/qml/dialogs/TabletAssetDialog.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,12 @@ Item {
|
||||||
return openModal;
|
return openModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component { id: assetDialogBuilder; TabletAssetDialog { } }
|
||||||
|
function assetDialog(properties) {
|
||||||
|
openModal = assetDialogBuilder.createObject(tabletRoot, properties);
|
||||||
|
return openModal;
|
||||||
|
}
|
||||||
|
|
||||||
function setMenuProperties(rootMenu, subMenu) {
|
function setMenuProperties(rootMenu, subMenu) {
|
||||||
tabletRoot.rootMenu = rootMenu;
|
tabletRoot.rootMenu = rootMenu;
|
||||||
tabletRoot.subMenu = subMenu;
|
tabletRoot.subMenu = subMenu;
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
||||||
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
|
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
|
||||||
|
static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation";
|
||||||
|
|
||||||
|
|
||||||
QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) {
|
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);
|
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
|
/// 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
|
/// might be in same thread as a script that sets the reticle to invisible
|
||||||
void WindowScriptingInterface::ensureReticleVisible() const {
|
void WindowScriptingInterface::ensureReticleVisible() const {
|
||||||
|
@ -202,6 +212,31 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString&
|
||||||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
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) {
|
void WindowScriptingInterface::showAssetServer(const QString& upload) {
|
||||||
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
|
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ public slots:
|
||||||
CustomPromptResult customPrompt(const QVariant& config);
|
CustomPromptResult customPrompt(const QVariant& config);
|
||||||
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||||
QScriptValue save(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 showAssetServer(const QString& upload = "");
|
||||||
void copyToClipboard(const QString& text);
|
void copyToClipboard(const QString& text);
|
||||||
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
|
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
|
||||||
|
@ -88,6 +89,9 @@ private:
|
||||||
QString getPreviousBrowseLocation() const;
|
QString getPreviousBrowseLocation() const;
|
||||||
void setPreviousBrowseLocation(const QString& location);
|
void setPreviousBrowseLocation(const QString& location);
|
||||||
|
|
||||||
|
QString getPreviousBrowseAssetLocation() const;
|
||||||
|
void setPreviousBrowseAssetLocation(const QString& location);
|
||||||
|
|
||||||
void ensureReticleVisible() const;
|
void ensureReticleVisible() const;
|
||||||
|
|
||||||
int createMessageBox(QString title, QString text, int buttons, int defaultButton);
|
int createMessageBox(QString title, QString text, int buttons, int defaultButton);
|
||||||
|
|
|
@ -13,28 +13,28 @@
|
||||||
#include "UserActivityLogger.h"
|
#include "UserActivityLogger.h"
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::enabledEdit() {
|
void UserActivityLoggerScriptingInterface::enabledEdit() {
|
||||||
logAction("enabled_edit");
|
doLogAction("enabled_edit");
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) {
|
void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) {
|
||||||
logAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
doLogAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::closedTablet() {
|
void UserActivityLoggerScriptingInterface::closedTablet() {
|
||||||
logAction("closed_tablet");
|
doLogAction("closed_tablet");
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::openedMarketplace() {
|
void UserActivityLoggerScriptingInterface::openedMarketplace() {
|
||||||
logAction("opened_marketplace");
|
doLogAction("opened_marketplace");
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) {
|
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,
|
void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete,
|
||||||
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
|
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
|
||||||
logAction("tutorial_progress", {
|
doLogAction("tutorial_progress", {
|
||||||
{ "tutorial_run_id", tutorialRunID },
|
{ "tutorial_run_id", tutorialRunID },
|
||||||
{ "tutorial_version", tutorialVersion },
|
{ "tutorial_version", tutorialVersion },
|
||||||
{ "step", stepName },
|
{ "step", stepName },
|
||||||
|
@ -52,11 +52,11 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar
|
||||||
if (target.length() > 0) {
|
if (target.length() > 0) {
|
||||||
payload["target"] = target;
|
payload["target"] = target;
|
||||||
}
|
}
|
||||||
logAction("pal_activity", payload);
|
doLogAction("pal_activity", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) {
|
void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) {
|
||||||
logAction("pal_opened", {
|
doLogAction("pal_opened", {
|
||||||
{ "seconds_opened", secondsOpened }
|
{ "seconds_opened", secondsOpened }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -68,10 +68,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b
|
||||||
if (detailsString.length() > 0) {
|
if (detailsString.length() > 0) {
|
||||||
payload["details"] = detailsString;
|
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",
|
QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction",
|
||||||
Q_ARG(QString, action),
|
Q_ARG(QString, action),
|
||||||
Q_ARG(QJsonObject, details));
|
Q_ARG(QJsonObject, details));
|
||||||
|
|
|
@ -29,9 +29,10 @@ public:
|
||||||
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
|
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
|
||||||
Q_INVOKABLE void palAction(QString action, QString target);
|
Q_INVOKABLE void palAction(QString action, QString target);
|
||||||
Q_INVOKABLE void palOpened(float secondsOpen);
|
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:
|
private:
|
||||||
void logAction(QString action, QJsonObject details = {});
|
void doLogAction(QString action, QJsonObject details = {});
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_UserActivityLoggerScriptingInterface_h
|
#endif // hifi_UserActivityLoggerScriptingInterface_h
|
||||||
|
|
|
@ -44,7 +44,8 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
|
||||||
|
|
||||||
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
||||||
if (callback.isFunction()) {
|
if (callback.isFunction()) {
|
||||||
QScriptValueList args { };
|
QString error = request->getErrorString();
|
||||||
|
QScriptValueList args { error };
|
||||||
callback.call(_engine->currentContext()->thisObject(), args);
|
callback.call(_engine->currentContext()->thisObject(), args);
|
||||||
}
|
}
|
||||||
request->deleteLater();
|
request->deleteLater();
|
||||||
|
|
|
@ -72,6 +72,7 @@ public:
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Called when setMapping is complete
|
* Called when setMapping is complete
|
||||||
* @callback Assets~setMappingCallback
|
* @callback Assets~setMappingCallback
|
||||||
|
* @param {string} error
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
||||||
|
|
||||||
|
|
|
@ -210,9 +210,11 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr
|
||||||
}
|
}
|
||||||
|
|
||||||
if (QThread::currentThread() != thread()) {
|
if (QThread::currentThread() != thread()) {
|
||||||
|
bool result;
|
||||||
QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection,
|
QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection,
|
||||||
|
Q_RETURN_ARG(bool, result),
|
||||||
Q_ARG(QScriptValue, getClipAtpUrl));
|
Q_ARG(QScriptValue, getClipAtpUrl));
|
||||||
return false;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_lastClip) {
|
if (!_lastClip) {
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
#include <NetworkAccessManager.h>
|
#include <NetworkAccessManager.h>
|
||||||
#include <PathUtils.h>
|
#include <PathUtils.h>
|
||||||
#include <ResourceScriptingInterface.h>
|
#include <ResourceScriptingInterface.h>
|
||||||
|
#include <UserActivityLoggerScriptingInterface.h>
|
||||||
#include <NodeList.h>
|
#include <NodeList.h>
|
||||||
#include <ScriptAvatarData.h>
|
#include <ScriptAvatarData.h>
|
||||||
#include <udt/PacketHeaders.h>
|
#include <udt/PacketHeaders.h>
|
||||||
|
@ -678,6 +679,8 @@ void ScriptEngine::init() {
|
||||||
registerGlobalObject("Model", new ModelScriptingInterface(this));
|
registerGlobalObject("Model", new ModelScriptingInterface(this));
|
||||||
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
|
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
|
||||||
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
|
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
|
||||||
|
|
||||||
|
registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
|
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
|
||||||
|
|
|
@ -716,6 +716,86 @@ QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption,
|
||||||
return DependencyManager::get<OffscreenUi>()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options);
|
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) {
|
bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) {
|
||||||
if (!filterEnabled(originalDestination, event)) {
|
if (!filterEnabled(originalDestination, event)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -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 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 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
|
// 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);
|
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
|
// Compatibility with QFileDialog::getSaveFileName
|
||||||
|
@ -125,6 +127,8 @@ public:
|
||||||
// Compatibility with QFileDialog::getExistingDirectory
|
// 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 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 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);
|
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);
|
QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current);
|
||||||
|
@ -155,6 +159,7 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString fileDialog(const QVariantMap& properties);
|
QString fileDialog(const QVariantMap& properties);
|
||||||
|
QString assetDialog(const QVariantMap& properties);
|
||||||
|
|
||||||
QQuickItem* _desktop { nullptr };
|
QQuickItem* _desktop { nullptr };
|
||||||
QQuickItem* _toolWindow { nullptr };
|
QQuickItem* _toolWindow { nullptr };
|
||||||
|
|
BIN
scripts/system/assets/sounds/countdown-tick.wav
Normal file
BIN
scripts/system/assets/sounds/countdown-tick.wav
Normal file
Binary file not shown.
BIN
scripts/system/assets/sounds/finish-recording.wav
Normal file
BIN
scripts/system/assets/sounds/finish-recording.wav
Normal file
Binary file not shown.
BIN
scripts/system/assets/sounds/start-recording.wav
Normal file
BIN
scripts/system/assets/sounds/start-recording.wav
Normal file
Binary file not shown.
218
scripts/system/html/css/record.css
Normal file
218
scripts/system/html/css/record.css
Normal 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;
|
||||||
|
}
|
298
scripts/system/html/js/record.js
Normal file
298
scripts/system/html/js/record.js
Normal 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();
|
||||||
|
}
|
87
scripts/system/html/record.html
Normal file
87
scripts/system/html/record.html
Normal 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 “Settings”.</li>
|
||||||
|
<li>In the “Scripts” 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’d like to run at a given time in the “Instances” slot.</li>
|
||||||
|
<li>Click “Save and restart”.</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>
|
439
scripts/system/playRecordingAC.js
Normal file
439
scripts/system/playRecordingAC.js
Normal 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
694
scripts/system/record.js
Normal 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);
|
||||||
|
}());
|
Loading…
Reference in a new issue