mirror of
https://github.com/overte-org/overte.git
synced 2025-04-08 11:54:20 +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 <SoundCache.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
|
||||
#include "AssignmentFactory.h"
|
||||
#include "AssignmentDynamicFactory.h"
|
||||
|
@ -66,6 +67,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
|||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
|
||||
// setup a thread for the NodeList and its PacketReceiver
|
||||
QThread* nodeThread = new QThread(this);
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
Component { id: assetDialogBuilder; AssetDialog { } }
|
||||
function assetDialog(properties) {
|
||||
return assetDialogBuilder.createObject(desktop, properties);
|
||||
}
|
||||
|
||||
function unfocusWindows() {
|
||||
// First find the active focus item, and unfocus it, all the way
|
||||
// up the parent chain to the window
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
Component { id: assetDialogBuilder; TabletAssetDialog { } }
|
||||
function assetDialog(properties) {
|
||||
openModal = assetDialogBuilder.createObject(tabletRoot, properties);
|
||||
return openModal;
|
||||
}
|
||||
|
||||
function setMenuProperties(rootMenu, subMenu) {
|
||||
tabletRoot.rootMenu = rootMenu;
|
||||
tabletRoot.subMenu = subMenu;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
||||
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
|
||||
static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation";
|
||||
|
||||
|
||||
QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) {
|
||||
|
@ -149,6 +150,15 @@ void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location
|
|||
Setting::Handle<QVariant>(LAST_BROWSE_LOCATION_SETTING).set(location);
|
||||
}
|
||||
|
||||
QString WindowScriptingInterface::getPreviousBrowseAssetLocation() const {
|
||||
QString ASSETS_ROOT_PATH = "/";
|
||||
return Setting::Handle<QString>(LAST_BROWSE_ASSETS_LOCATION_SETTING, ASSETS_ROOT_PATH).get();
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& location) {
|
||||
Setting::Handle<QVariant>(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location);
|
||||
}
|
||||
|
||||
/// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and
|
||||
/// might be in same thread as a script that sets the reticle to invisible
|
||||
void WindowScriptingInterface::ensureReticleVisible() const {
|
||||
|
@ -202,6 +212,31 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString&
|
|||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
||||
}
|
||||
|
||||
/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid
|
||||
/// directory the browser will start at the root directory.
|
||||
/// \param const QString& title title of the window
|
||||
/// \param const QString& directory directory to start the asset browser at
|
||||
/// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog`
|
||||
/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue`
|
||||
QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) {
|
||||
ensureReticleVisible();
|
||||
QString path = directory;
|
||||
if (path.isEmpty()) {
|
||||
path = getPreviousBrowseAssetLocation();
|
||||
}
|
||||
if (path.left(1) != "/") {
|
||||
path = "/" + path;
|
||||
}
|
||||
if (path.right(1) != "/") {
|
||||
path = path + "/";
|
||||
}
|
||||
QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter);
|
||||
if (!result.isEmpty()) {
|
||||
setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath());
|
||||
}
|
||||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::showAssetServer(const QString& upload) {
|
||||
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ public slots:
|
|||
CustomPromptResult customPrompt(const QVariant& config);
|
||||
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
void showAssetServer(const QString& upload = "");
|
||||
void copyToClipboard(const QString& text);
|
||||
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
|
||||
|
@ -88,6 +89,9 @@ private:
|
|||
QString getPreviousBrowseLocation() const;
|
||||
void setPreviousBrowseLocation(const QString& location);
|
||||
|
||||
QString getPreviousBrowseAssetLocation() const;
|
||||
void setPreviousBrowseAssetLocation(const QString& location);
|
||||
|
||||
void ensureReticleVisible() const;
|
||||
|
||||
int createMessageBox(QString title, QString text, int buttons, int defaultButton);
|
||||
|
|
|
@ -13,28 +13,28 @@
|
|||
#include "UserActivityLogger.h"
|
||||
|
||||
void UserActivityLoggerScriptingInterface::enabledEdit() {
|
||||
logAction("enabled_edit");
|
||||
doLogAction("enabled_edit");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) {
|
||||
logAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
||||
doLogAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::closedTablet() {
|
||||
logAction("closed_tablet");
|
||||
doLogAction("closed_tablet");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::openedMarketplace() {
|
||||
logAction("opened_marketplace");
|
||||
doLogAction("opened_marketplace");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) {
|
||||
logAction("toggled_away", { { "is_away", isAway } });
|
||||
doLogAction("toggled_away", { { "is_away", isAway } });
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete,
|
||||
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
|
||||
logAction("tutorial_progress", {
|
||||
doLogAction("tutorial_progress", {
|
||||
{ "tutorial_run_id", tutorialRunID },
|
||||
{ "tutorial_version", tutorialVersion },
|
||||
{ "step", stepName },
|
||||
|
@ -52,11 +52,11 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar
|
|||
if (target.length() > 0) {
|
||||
payload["target"] = target;
|
||||
}
|
||||
logAction("pal_activity", payload);
|
||||
doLogAction("pal_activity", payload);
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) {
|
||||
logAction("pal_opened", {
|
||||
doLogAction("pal_opened", {
|
||||
{ "seconds_opened", secondsOpened }
|
||||
});
|
||||
}
|
||||
|
@ -68,10 +68,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b
|
|||
if (detailsString.length() > 0) {
|
||||
payload["details"] = detailsString;
|
||||
}
|
||||
logAction("makeUserConnection", payload);
|
||||
doLogAction("makeUserConnection", payload);
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) {
|
||||
void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) {
|
||||
doLogAction(action, QJsonObject::fromVariantMap(details));
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::doLogAction(QString action, QJsonObject details) {
|
||||
QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction",
|
||||
Q_ARG(QString, action),
|
||||
Q_ARG(QJsonObject, details));
|
||||
|
|
|
@ -29,9 +29,10 @@ public:
|
|||
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
|
||||
Q_INVOKABLE void palAction(QString action, QString target);
|
||||
Q_INVOKABLE void palOpened(float secondsOpen);
|
||||
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details="");
|
||||
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = "");
|
||||
Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{});
|
||||
private:
|
||||
void logAction(QString action, QJsonObject details = {});
|
||||
void doLogAction(QString action, QJsonObject details = {});
|
||||
};
|
||||
|
||||
#endif // hifi_UserActivityLoggerScriptingInterface_h
|
||||
|
|
|
@ -44,7 +44,8 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
|
|||
|
||||
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QScriptValueList args { };
|
||||
QString error = request->getErrorString();
|
||||
QScriptValueList args { error };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
request->deleteLater();
|
||||
|
|
|
@ -72,6 +72,7 @@ public:
|
|||
/**jsdoc
|
||||
* Called when setMapping is complete
|
||||
* @callback Assets~setMappingCallback
|
||||
* @param {string} error
|
||||
*/
|
||||
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
||||
|
||||
|
|
|
@ -210,9 +210,11 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr
|
|||
}
|
||||
|
||||
if (QThread::currentThread() != thread()) {
|
||||
bool result;
|
||||
QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(QScriptValue, getClipAtpUrl));
|
||||
return false;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!_lastClip) {
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
#include <NetworkAccessManager.h>
|
||||
#include <PathUtils.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
#include <NodeList.h>
|
||||
#include <ScriptAvatarData.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
@ -678,6 +679,8 @@ void ScriptEngine::init() {
|
|||
registerGlobalObject("Model", new ModelScriptingInterface(this));
|
||||
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
|
||||
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
|
||||
|
||||
registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||
}
|
||||
|
||||
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
|
||||
|
|
|
@ -716,6 +716,86 @@ QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption,
|
|||
return DependencyManager::get<OffscreenUi>()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
class AssetDialogListener : public ModalDialogListener {
|
||||
// ATP equivalent of FileDialogListener.
|
||||
Q_OBJECT
|
||||
|
||||
friend class OffscreenUi;
|
||||
AssetDialogListener(QQuickItem* messageBox) : ModalDialogListener(messageBox) {
|
||||
if (_finished) {
|
||||
return;
|
||||
}
|
||||
connect(_dialog, SIGNAL(selectedAsset(QVariant)), this, SLOT(onSelectedAsset(QVariant)));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void onSelectedAsset(QVariant asset) {
|
||||
_result = asset;
|
||||
_finished = true;
|
||||
disconnect(_dialog);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
QString OffscreenUi::assetDialog(const QVariantMap& properties) {
|
||||
// ATP equivalent of fileDialog().
|
||||
QVariant buildDialogResult;
|
||||
bool invokeResult;
|
||||
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
|
||||
TabletProxy* tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"));
|
||||
if (tablet->getToolbarMode()) {
|
||||
invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(properties)));
|
||||
} else {
|
||||
QQuickItem* tabletRoot = tablet->getTabletRoot();
|
||||
invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(properties)));
|
||||
emit tabletScriptingInterface->tabletNotification();
|
||||
}
|
||||
|
||||
if (!invokeResult) {
|
||||
qWarning() << "Failed to create asset open dialog";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QVariant result = AssetDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult();
|
||||
if (!result.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
qCDebug(uiLogging) << result.toString();
|
||||
return result.toUrl().toString();
|
||||
}
|
||||
|
||||
QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
|
||||
// ATP equivalent of fileOpenDialog().
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
QMetaObject::invokeMethod(this, "assetOpenDialog", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QString, result),
|
||||
Q_ARG(QString, caption),
|
||||
Q_ARG(QString, dir),
|
||||
Q_ARG(QString, filter),
|
||||
Q_ARG(QString*, selectedFilter),
|
||||
Q_ARG(QFileDialog::Options, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
// FIXME support returning the selected filter... somehow?
|
||||
QVariantMap map;
|
||||
map.insert("caption", caption);
|
||||
map.insert("dir", dir);
|
||||
map.insert("filter", filter);
|
||||
map.insert("options", static_cast<int>(options));
|
||||
return assetDialog(map);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
// ATP equivalent of getOpenFileName().
|
||||
return DependencyManager::get<OffscreenUi>()->assetOpenDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) {
|
||||
if (!filterEnabled(originalDestination, event)) {
|
||||
return false;
|
||||
|
|
|
@ -118,6 +118,8 @@ public:
|
|||
Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
// Compatibility with QFileDialog::getOpenFileName
|
||||
static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
// Compatibility with QFileDialog::getSaveFileName
|
||||
|
@ -125,6 +127,8 @@ public:
|
|||
// Compatibility with QFileDialog::getExistingDirectory
|
||||
static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant());
|
||||
Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config);
|
||||
QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current);
|
||||
|
@ -155,6 +159,7 @@ signals:
|
|||
|
||||
private:
|
||||
QString fileDialog(const QVariantMap& properties);
|
||||
QString assetDialog(const QVariantMap& properties);
|
||||
|
||||
QQuickItem* _desktop { nullptr };
|
||||
QQuickItem* _toolWindow { nullptr };
|
||||
|
|
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