3
0
Fork 0
mirror of https://thingvellir.net/git/overte synced 2025-03-27 23:52:03 +01:00

Merge pull request from kasenvr/master

Upgrading
This commit is contained in:
Alezia Kurdis 2020-11-16 21:34:14 -05:00 committed by GitHub
commit 4c8d8e1b3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 913 additions and 710 deletions

View file

@ -3,114 +3,60 @@ name: Master CI Build
on:
push:
branches:
- master
- gha-master-ci
# FIXME: Change target branch to "master" before merging into "master" branch.
env:
#APP_NAME: gpu-frame-player
APP_NAME: interface
BUILD_TYPE: Release
BUCKET_NAME: hifi-gh-builds
BUILD_NUMBER: ${{ github.run_number }}
CI_BUILD: Github
CMAKE_BACKTRACE_URL: https://highfidelity.sp.backtrace.io:6098
CMAKE_BACKTRACE_TOKEN: ${{ secrets.backtrace_token }}
CMAKE_BACKTRACE_SYMBOLS_TOKEN: ${{ secrets.backtrace_symbols_token }}
GIT_COMMIT: ${{ github.sha }}
HIFI_VCPKG_BOOTSTRAP: true
LAUNCHER_HMAC_SECRET: ${{ secrets.launcher_hmac_secret }}
OCULUS_APP_ID: '${{ secrets.oculus_app_id }}'
# VCPKG did not build well on OSX disabling HIFI_VCPKG_BOOTSTRAP, which invokes a download to a working version of vcpkg
# HIFI_VCPKG_BOOTSTRAP: true
RELEASE_TYPE: PRODUCTION
RELEASE_DYNAMODB_V2: ReleaseManager2-ReleaseQueue-prod
RELEASE_NUMBER: ${{ github.run_number }}
STABLE_BUILD: 0
UPLOAD_BUCKET: athena-public
# OSX specific variables
# OSX-specific variables
DEVELOPER_DIR: /Applications/Xcode_11.2.app/Contents/Developer
MACOSX_DEPLOYMENT_TARGET: '10.11'
# WIN32 specific variables
# WIN-specific variables
PreferredToolArchitecture: X64
# Mac OS
#PLATFORM_CMAKE_GENERATOR=Xcode
#PLATFORM_BUILD_ARGUMENTS=--config Release --target package
#ARTIFACT_EXPRESSION=build/*.dmg,build/*.zip
# Windows
#PLATFORM_CMAKE_GENERATOR=Visual Studio 15 2017 Win64
#PLATFORM_BUILD_ARGUMENTS=--target package --config release
#ARTIFACT_EXPRESSION=build/*.exe,build/*.zip,*-symbols.zip
# Ubuntu
#PLATFORM_CMAKE_GENERATOR=Unix Makefiles
#PLATFORM_BUILD_ARGUMENTS=--target all -- -j4
#ARTIFACT_EXPRESSION=build/assignment-client/**,build/domain-server/**,build/ice-server/ice-server,build/tools/ice-client/ice-client,build/tools/ac-client/ac-client,build/tools/oven,build/ext/makefiles/nvtt/project/lib/**,build/ext/makefiles/quazip/project/lib/**
# Android
# branch: master
# GA_TRACKING_ID: ${{ secrets.ga_tracking_id }}
# ANDROID_OAUTH_CLIENT_SECRET=${MASKED_ANDROID_OAUTH_CLIENT_SECRET_NIGHTLY}
# ANDROID_OAUTH_CLIENT_ID=6c7d2349c0614640150db37457a1f75dce98a28ffe8f14d47f6cfae4de5b262a
# ANDROID_OAUTH_REDIRECT_URI=https://dev-android-interface.highfidelity.com/auth
# branch: !master
# GA_TRACKING_ID=UA-39558647-11
# ANDROID_OAUTH_CLIENT_SECRET=${MASKED_ANDROID_OAUTH_CLIENT_SECRET_RELEASE}
# ANDROID_OAUTH_CLIENT_ID= c1063ea5d0b0c405e0c9cd77351328e211a91496a3f25985a99e861f1661db1d
# ANDROID_OAUTH_REDIRECT_URI=https://android-interface.highfidelity.com/auth
# ARTIFACT_EXPRESSION=android/*.apk
# ANDROID_APK_NAME=HighFidelity-Beta-PR${RELEASE_NUMBER}-${GIT_COMMIT_SHORT}.apk
# ANDROID_BUILT_APK_NAME=interface-debug.apk
# ANDROID_APP=interface
# ANDROID_BUILD_DIR=debug
# ANDROID_BUILD_TARGET=assembleDebug
# STABLE_BUILD=0
jobs:
generate_build_number:
runs-on: ubuntu-latest
steps:
- name: Generate build number
id: buildnumber
uses: highfidelity/build-number@v3
with:
token: ${{secrets.github_token}}
- name: Upload build number
uses: actions/upload-artifact@v1
with:
name: BUILD_NUMBER
path: BUILD_NUMBER
build:
strategy:
matrix:
os: [windows-latest, macOS-latest]
build_type: [full, client]
#os: [windows-latest, macOS-latest, ubuntu-latest]
# exclude:
# - os: ubuntu-latest
# build_type: client
os: [windows-latest, macOS-latest, ubuntu-18.04]
# build_type: [full, client]
build_type: [full]
include:
- os: ubuntu-18.04
build_type: full
apt-dependencies: mesa-common-dev libegl1 libglvnd-dev libdouble-conversion1 libpulse0
fail-fast: false
runs-on: ${{matrix.os}}
needs: generate_build_number
steps:
- name: Download build number
uses: actions/download-artifact@v1
with:
name: BUILD_NUMBER
- name: Restore build number
id: buildnumber
uses: highfidelity/build-number@v3
with:
output_name: RELEASE_NUMBER
- name: Configure Build Environment 1
- name: Report Build Number
shell: bash
run: |
echo "Build number: $BUILD_NUMBER"
- name: Configure build environment 1
shell: bash
id: buildenv1
run: |
echo ::set-env name=UPLOAD_PREFIX::master
echo ::set-env name=GIT_COMMIT_SHORT::`echo $GIT_COMMIT | cut -c1-7`
echo ::set-env name=JOB_NAME::"build (${{matrix.os}}, ${{matrix.build_type}})"
# Linux build variables
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
if [[ "${{ matrix.os }}" = "ubuntu-"* ]]; then
echo ::set-env name=PYTHON_EXEC::python3
echo ::set-env name=INSTALLER_EXT::tgz
echo ::set-env name=CMAKE_BUILD_EXTRA::"-- -j3"
echo ::set-env name=CMAKE_EXTRA::"-DBUILD_TOOLS:BOOLEAN=FALSE -DHIFI_PYTHON_EXEC:FILEPATH=$(which python3)"
fi
# Mac build variables
if [ "${{ matrix.os }}" = "macOS-latest" ]; then
@ -118,8 +64,8 @@ jobs:
echo ::set-env name=ZIP_COMMAND::zip
echo ::set-env name=ZIP_ARGS::-r
echo ::set-env name=INSTALLER_EXT::dmg
echo ::set-env name=SYMBOL_REGEX::dSYM
echo "::set-output name=symbols_archive::${{ steps.buildnumber.outputs.build_number }}-${{ matrix.build_type }}-mac-symbols.zip"
echo ::set-env name=CMAKE_EXTRA::"-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=OFF -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -G Xcode"
echo "::set-output name=symbols_archive::${BUILD_NUMBER}-${{ matrix.build_type }}-mac-symbols.zip"
fi
# Windows build variables
if [ "${{ matrix.os }}" = "windows-latest" ]; then
@ -127,40 +73,28 @@ jobs:
echo ::set-env name=ZIP_COMMAND::7z
echo ::set-env name=ZIP_ARGS::a
echo ::set-env name=INSTALLER_EXT::exe
echo ::set-env name=CMAKE_EXTRA::"-A x64"
echo "::set-env name=SYMBOL_REGEX::\(exe\|dll\|pdb\)"
echo "::set-output name=symbols_archive::${{ steps.buildnumber.outputs.build_number }}-${{ matrix.build_type }}-win-symbols.zip"
echo "::set-output name=symbols_archive::${BUILD_NUMBER}-${{ matrix.build_type }}-win-symbols.zip"
# echo ::set-env name=HF_PFX_PASSPHRASE::${{secrets.pfx_key}}
# echo "::set-env name=HF_PFX_FILE::${{runner.workspace}}\build\codesign.pfx"
fi
# Configuration is broken into two steps because you can't set an env var and also reference it in the same step
- name: Configure Build Environment 2
- name: Configure build environment 2
shell: bash
run: |
echo "${{ steps.buildenv1.outputs.symbols_archive }}"
echo ::set-env name=ARTIFACT_PATTERN::HighFidelity-Beta-*.$INSTALLER_EXT
echo ::set-env name=ARTIFACT_PATTERN::Vircadia-Alpha-*.$INSTALLER_EXT
# Build type variables
if [ "${{ matrix.build_type }}" = "full" ]; then
echo ::set-env name=CLIENT_ONLY::FALSE
echo ::set-env name=INSTALLER::HighFidelity-Beta-$RELEASE_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT
echo ::set-env name=INSTALLER::Vircadia-Alpha-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT
else
echo ::set-env name=CLIENT_ONLY::TRUE
echo ::set-env name=INSTALLER::HighFidelity-Beta-Interface-$RELEASE_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT
echo ::set-env name=INSTALLER::Vircadia-Alpha-Interface-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT
fi
# Linux build variables
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
echo ::set-env name=PYTHON_EXEC::python3
echo ::set-env name=CMAKE_EXTRA::""
fi
# Mac build variables
if [ "${{ matrix.os }}" = "macOS-latest" ]; then
echo ::set-env name=CMAKE_EXTRA::"-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=OFF -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -G Xcode"
fi
# Windows build variables
if [ "${{ matrix.os }}" = "windows-latest" ]; then
echo ::set-env name=CMAKE_EXTRA::"-A x64"
echo ::set-env name=HF_PFX_PASSPHRASE::${{secrets.pfx_key}}
echo "::set-env name=HF_PFX_FILE::${{runner.workspace}}\build\codesign.pfx"
fi
- name: Clear Working Directory
if: matrix.os == 'windows-latest'
- name: Clear working directory
if: startsWith(matrix.os, 'windows')
shell: bash
working-directory: ${{runner.workspace}}
run: rm -rf ./*
@ -168,89 +102,119 @@ jobs:
with:
submodules: true
fetch-depth: 1
- name: Create Build Directory
run: cmake -E make_directory ${{runner.workspace}}/build
- name: Decrypt Signing Key (Windows)
if: matrix.os == 'windows-latest'
working-directory: ${{runner.workspace}}/build
- name: Install dependencies
if: startsWith(matrix.os, 'ubuntu')
shell: bash
run: gpg --batch --yes -o codesign.pfx --passphrase "${{secrets.gpg_symmetric_key}}" --decrypt $GITHUB_WORKSPACE/tools/ci-scripts/codesign.pfx.gpg
- name: Import Signing Key (Windows)
if: matrix.os == 'windows-latest'
working-directory: ${{runner.workspace}}/build
shell: powershell
run: |
$mypwd=ConvertTo-SecureString -String ${{ secrets.pfx_key }} -Force -AsPlainText
Import-PfxCertificate -Password $mypwd -CertStoreLocation Cert:\CurrentUser\My -FilePath ${{runner.workspace}}\build\codesign.pfx
Import-PfxCertificate -Password $mypwd -CertStoreLocation Cert:\LocalMachine\My -FilePath ${{runner.workspace}}\build\codesign.pfx
run: |
echo "Installing Python Modules:"
pip3 install distro || exit 1
echo "Updating apt repository index"
sudo apt update || exit 1
echo "Installing apt packages"
sudo apt install -y ${{ matrix.apt-dependencies }} || exit 1
- name: Install Python modules
if: matrix.os != 'ubuntu-latest'
if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
shell: bash
run: $PYTHON_EXEC -m pip install boto3 PyGithub
- name: Create build environment
shell: bash
run: cmake -E make_directory "${{runner.workspace}}/build"
- name: Configure CMake
working-directory: ${{runner.workspace}}/build
shell: bash
run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCLIENT_ONLY:BOOLEAN=$CLIENT_ONLY $CMAKE_EXTRA
- name: Build Application
run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DVCPKG_BUILD_TYPE=release -DCLIENT_ONLY:BOOLEAN=$CLIENT_ONLY -DBYPASS_SIGNING:BOOLEAN=TRUE $CMAKE_EXTRA
- name: Build application
working-directory: ${{runner.workspace}}/build
shell: bash
run: cmake --build . --config $BUILD_TYPE --target $APP_NAME
- name: Build Console
run: cmake --build . --config $BUILD_TYPE --target $APP_NAME $CMAKE_BUILD_EXTRA
- name: Build domain server
working-directory: ${{runner.workspace}}/build
shell: bash
run: cmake --build . --config $BUILD_TYPE --target packaged-server-console
- name: Build Domain Server (FullBuild)
if: matrix.build_type == 'full'
shell: bash
working-directory: ${{runner.workspace}}/build
run: cmake --build . --config $BUILD_TYPE --target domain-server
- name: Build Assignment Client (FullBuild)
if: matrix.build_type == 'full'
shell: bash
working-directory: ${{runner.workspace}}/build
run: cmake --build . --config $BUILD_TYPE --target assignment-client
- name: Build Installer
run: cmake --build . --config $BUILD_TYPE --target domain-server $CMAKE_BUILD_EXTRA
- name: Build assignment client
working-directory: ${{runner.workspace}}/build
shell: bash
run: cmake --build . --config $BUILD_TYPE --target package
- name: Sign Installer (Windows)
if: matrix.os == 'windows-latest'
shell: powershell
working-directory: C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64
run: .\signtool.exe sign /fd sha256 /f ${{runner.workspace}}\build\codesign.pfx /p ${{secrets.pfx_key}} /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 ${{runner.workspace}}\build\${env:INSTALLER}
- name: Upload Artifact
if: matrix.os != 'ubuntu-latest'
run: cmake --build . --config $BUILD_TYPE --target assignment-client $CMAKE_BUILD_EXTRA
- name: Build console
working-directory: ${{runner.workspace}}/build
shell: bash
run: cmake --build . --config $BUILD_TYPE --target packaged-server-console $CMAKE_BUILD_EXTRA
- name: Build installer
working-directory: ${{runner.workspace}}/build
shell: bash
run: |
echo "Retry code from https://unix.stackexchange.com/a/137639"
function fail {
echo $1 >&2
exit 1
}
function retry {
local n=1
local max=5
local delay=15
while true; do
"$@" && break || {
if [[ $n -lt $max ]]; then
((n++))
echo "Command failed. Attempt $n/$max:"
sleep $delay;
else
fail "The command has failed after $n attempts."
fi
}
done
}
retry cmake --build . --config $BUILD_TYPE --target package $CMAKE_BUILD_EXTRA
#- name: Sign installer (Windows)
# if: startsWith(matrix.os, 'windows')
# shell: powershell
# working-directory: C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64
# run: .\signtool.exe sign /fd sha256 /f ${{runner.workspace}}\build\codesign.pfx /p ${{secrets.pfx_key}} /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 ${{runner.workspace}}\build\${env:INSTALLER}
- name: Output system stats
if: ${{ always() }}
working-directory: ${{runner.workspace}}/build
shell: bash
run: |
echo "Disk usage:"
df -h
- name: Output installer logs
if: failure() && startsWith(matrix.os, 'windows')
shell: bash
working-directory: ${{runner.workspace}}/build
run: cat ./_CPack_Packages/win64/NSIS/NSISOutput.log
- name: Upload artifact
if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
shell: bash
working-directory: ${{runner.workspace}}/build
env:
AWS_ACCESS_KEY_ID: ${{ secrets.aws_access_key_id }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.aws_secret_access_key }}
run: $PYTHON_EXEC $GITHUB_WORKSPACE/tools/ci-scripts/upload.py
- name: Archive Symbols
if: (matrix.os == 'windows-latest') || (matrix.os == 'macOS-latest')
working-directory: ${{runner.workspace}}
shell: bash
run: |
SYMBOLS_TEMP="symbols-temp"
mkdir $SYMBOLS_TEMP
find "./build" \( -path '*/tools/gpu-frame-player/*' -or -path '*/interface/*' -or -path '*/plugins/*' \) -regex ".*\.$SYMBOL_REGEX" -exec cp -r {} $SYMBOLS_TEMP \;
cd $SYMBOLS_TEMP
$ZIP_COMMAND $ZIP_ARGS ../${{ steps.buildenv1.outputs.symbols_archive }} .
- name: Upload Symbols
if: (matrix.os == 'windows-latest') || (matrix.os == 'macOS-latest')
working-directory: ${{runner.workspace}}
shell: bash
run: |
curl --data-binary @${{ steps.buildenv1.outputs.symbols_archive }} "$CMAKE_BACKTRACE_URL/post?format=symbols&token=$CMAKE_BACKTRACE_SYMBOLS_TOKEN&upload_file=${{steps.buildenv1.outputs.symbols_archive}}&tag=$RELEASE_NUMBER"
# - name: Debug List Symbols
# if: (matrix.os == 'windows-latest') || (matrix.os == 'macOS-latest')
# working-directory: ${{runner.workspace}}
# shell: bash
# run: |
# unzip -v "${{runner.workspace}}/${{ steps.buildenv1.outputs.symbols_archive }}"
# - name: Debug Upload Symbols Artifact
# if: (matrix.os == 'windows-latest') || (matrix.os == 'macOS-latest')
# uses: actions/upload-artifact@v1
# with:
# name: symbols
# path: ${{runner.workspace}}/${{ steps.buildenv1.outputs.symbols_archive }}
#- name: Archive symbols
# if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
# working-directory: ${{runner.workspace}}
# shell: bash
# run: |
# SYMBOLS_TEMP="symbols-temp"
# mkdir $SYMBOLS_TEMP
# find "./build" \( -path '*/tools/gpu-frame-player/*' -or -path '*/interface/*' -or -path '*/plugins/*' \) -regex ".*\.$SYMBOL_REGEX" -exec cp -r {} $SYMBOLS_TEMP \;
# cd $SYMBOLS_TEMP
# $ZIP_COMMAND $ZIP_ARGS ../${{ steps.buildenv1.outputs.symbols_archive }} .
#- name: Upload symbols
# if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
# working-directory: ${{runner.workspace}}
# shell: bash
# run: |
# curl --data-binary @${{ steps.buildenv1.outputs.symbols_archive }} "$CMAKE_BACKTRACE_URL/post?format=symbols&token=$CMAKE_BACKTRACE_SYMBOLS_TOKEN&upload_file=${{steps.buildenv1.outputs.symbols_archive}}&tag=$BUILD_NUMBER"
#- name: Debug list symbols
# if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
# working-directory: ${{runner.workspace}}
# shell: bash
# run: |
# unzip -v "${{runner.workspace}}/${{ steps.buildenv1.outputs.symbols_archive }}"
#- name: Upload debug list symbols
# if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'macOS')
# uses: actions/upload-artifact@v1
# with:
# name: symbols
# path: ${{runner.workspace}}/${{ steps.buildenv1.outputs.symbols_archive }}

View file

@ -470,7 +470,9 @@ void EntityScriptServer::resetEntitiesScriptEngine() {
scriptEngines->runScriptInitializers(newEngine);
newEngine->runInThread();
auto newEngineSP = qSharedPointerCast<EntitiesScriptEngineProvider>(newEngine);
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngineSP);
// On the entity script server, these are the same
DependencyManager::get<EntityScriptingInterface>()->setPersistentEntitiesScriptEngine(newEngineSP);
DependencyManager::get<EntityScriptingInterface>()->setNonPersistentEntitiesScriptEngine(newEngineSP);
if (_entitiesScriptEngine) {
disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,

View file

@ -202,9 +202,9 @@
; The Inner invocation has written an uninstaller binary for us.
; We need to sign it if it's a production or PR build.
!if @PRODUCTION_BUILD@ == 1
!if @BYPASS_SIGNING@ == 1
!if @BYPASS_SIGNING@ == TRUE
!warning "BYPASS_SIGNING set - installer will not be signed"
!else
!else
!system '"@SIGNTOOL_EXECUTABLE@" sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://timestamp.comodoca.com?td=sha256 /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0
!endif
!endif

View file

@ -365,7 +365,7 @@ $(document).ready(function(){
confirmButtonText: "Create",
closeOnConfirm: false,
html: true
}, function(inputValue){
}, function (inputValue) {
if (inputValue === false) {
swal.close();
@ -375,7 +375,7 @@ $(document).ready(function(){
}
} else {
// we're going to change the alert to a new one with a spinner while we create this domain
showSpinnerAlert('Creating domain ID');
// showSpinnerAlert('Creating domain ID');
createNewDomainID(inputValue, justConnected);
}
});
@ -387,7 +387,12 @@ $(document).ready(function(){
"label": label
}
$.post("/api/domains", domainJSON, function(data){
$.post("/api/domains", domainJSON, function(data) {
if (data.status === "failure") {
failedToCreateDomainID(data, justConnected);
return;
}
// we successfully created a domain ID, set it on that field
var domainID = data.domain.domainId;
console.log("Setting domain id to ", data, domainID);
@ -408,40 +413,50 @@ $(document).ready(function(){
text: successText,
html: true,
confirmButtonText: 'Save'
}, function(){
}, function () {
saveSettings();
});
}, 'json').fail(function(){
}, 'json').fail(function (data) {
failedToCreateDomainID(data, justConnected);
});
}
function failedToCreateDomainID(data, justConnected) {
var errorText = "There was a problem creating your new domain ID. Do you want to try again or";
var errorText = "There was a problem creating your new domain ID. Do you want to try again or";
if (data && data.status === "failure") {
errorText = "Error: " + data.error + "</br>Do you want to try again or";
console.log("Error: " + data.error);
} else {
console.log("Error: Failed to post to metaverse.");
}
if (justConnected) {
errorText += " just save your new access token?</br></br>You can always create a new domain ID later.";
if (justConnected) {
errorText += " just save your new access token?</br></br>You can always create a new domain ID later.";
} else {
errorText += " cancel?"
}
// we failed to create the new domain ID, show a sweet-alert that lets them try again or cancel
swal({
title: '',
type: 'error',
text: errorText,
html: true,
confirmButtonText: 'Try again',
showCancelButton: true,
closeOnConfirm: false
}, function (isConfirm) {
if (isConfirm) {
// they want to try creating a domain ID again
showDomainCreationAlert(justConnected);
} else {
errorText += " cancel?"
}
// we failed to create the new domain ID, show a sweet-alert that lets them try again or cancel
swal({
title: '',
type: 'error',
text: errorText,
html: true,
confirmButtonText: 'Try again',
showCancelButton: true,
closeOnConfirm: false
}, function(isConfirm){
if (isConfirm) {
// they want to try creating a domain ID again
showDomainCreationAlert(justConnected);
} else {
// they want to cancel
if (justConnected) {
// since they just connected we need to save the access token here
saveSettings();
}
// they want to cancel
if (justConnected) {
// since they just connected we need to save the access token here
saveSettings();
}
});
}
});
}

View file

@ -1238,7 +1238,7 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString&
// Get data pertaining to "me", the user who generated the access token.
const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me";
const QString WORDPRESS_USER_QUERY = "_fields=username,roles";
const QString WORDPRESS_USER_QUERY = "_fields=username,email,roles";
QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE + (apiBase.contains("?") ? "&" : "?") + WORDPRESS_USER_QUERY;
QNetworkRequest request;
@ -1270,8 +1270,13 @@ void DomainGatekeeper::requestDomainUserFinished() {
if (200 <= httpStatus && httpStatus < 300) {
QString username = rootObject.value("username").toString().toLower();
if (_inFlightDomainUserIdentityRequests.contains(username)) {
QString email = rootObject.value("email").toString().toLower();
if (_inFlightDomainUserIdentityRequests.contains(username) || _inFlightDomainUserIdentityRequests.contains(email)) {
// Success! Verified user.
if (!_inFlightDomainUserIdentityRequests.contains(username)) {
username = email;
}
_verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username));
_inFlightDomainUserIdentityRequests.remove(username);

View file

@ -114,7 +114,7 @@ Item {
displayNameField.placeholderText = "Display Name (optional)";
var savedDisplayName = Settings.getValue("Avatar/displayName", "");
displayNameField.text = savedDisplayName;
emailField.placeholderText = (!isLoggingInToDomain) ? "Username or Email" : "Username";
emailField.placeholderText = "Username or Email";
if (!isLoggingInToDomain) {
var savedUsername = Settings.getValue("keepMeLoggedIn/savedUsername", "");
emailField.text = keepMeLoggedInCheckbox.checked ? savedUsername === "Unknown user" ? "" : savedUsername : "";

View file

@ -312,9 +312,9 @@ Rectangle {
parent.color = hifi.colors.blueHighlight;
}
onClicked: {
lightboxPopup.titleText = "Script Plugin Infrastructure by Kasen";
lightboxPopup.titleText = "Script Plugin Infrastructure";
lightboxPopup.bodyText = "Toggles the activation of scripting plugins in the 'plugins/scripting' folder. \n\n"
+ "Created by https://kasen.io/";
+ "Created by:\n humbletim@gmail.com\n kasenvr@gmail.com";
lightboxPopup.button1text = "OK";
lightboxPopup.button1method = function() {
lightboxPopup.visible = false;

View file

@ -355,6 +355,7 @@ static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop";
static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin";
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn";
static const QString CACHEBUST_SCRIPT_REQUIRE_SETTING_NAME = "cachebustScriptRequire";
static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f;
@ -1966,6 +1967,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
loadSettings();
updateVerboseLogging();
setCachebustRequire();
// Make sure we don't time out during slow operations at startup
updateHeartbeat();
@ -2600,6 +2603,16 @@ void Application::updateVerboseLogging() {
QLoggingCategory::setFilterRules(rules);
}
void Application::setCachebustRequire() {
auto menu = Menu::getInstance();
if (!menu) {
return;
}
bool enable = menu->isOptionChecked(MenuOption::CachebustRequire);
Setting::Handle<bool>{ CACHEBUST_SCRIPT_REQUIRE_SETTING_NAME, false }.set(enable);
}
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) {
DomainHandler::ConnectionRefusedReason reasonCode = static_cast<DomainHandler::ConnectionRefusedReason>(reasonCodeInt);

View file

@ -472,6 +472,8 @@ public slots:
void setIsInterstitialMode(bool interstitialMode);
void updateVerboseLogging();
void setCachebustRequire();
void changeViewAsNeeded(float boomLength);

View file

@ -4,14 +4,14 @@
//
// Created by Stephen Birarda on 8/12/13.
// Copyright 2013 High Fidelity, Inc.
// Copyright 2020 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// For happ(ier) development of QML, use these two things:
// This forces QML files to be pulled from the source as you edit it: set environment variable HIFI_USE_SOURCE_TREE_RESOURCES=1
// Use this to live reload: DependencyManager::get<OffscreenUi>()->clearCache();
// For happ(ier) development of QML, use these two things:
// This forces QML files to be pulled from the source as you edit it: set environment variable HIFI_USE_SOURCE_TREE_RESOURCES=1
// Use this to live reload: DependencyManager::get<OffscreenUi>()->clearCache();
#include "Menu.h"
#include <QDesktopServices>
@ -365,6 +365,10 @@ Menu::Menu() {
// Developer > Scripting > Verbose Logging
addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::VerboseLogging, 0, false,
qApp, SLOT(updateVerboseLogging()));
// Developer > Scripting > Enable Cachebusting of Script.require
addCheckableActionToQMenuAndActionHash(scriptingOptionsMenu, MenuOption::CachebustRequire, 0, false,
qApp, SLOT(setCachebustRequire()));
// Developer > Scripting > Enable Speech Control API
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)

View file

@ -4,6 +4,7 @@
//
// Created by Stephen Birarda on 8/12/13.
// Copyright 2013 High Fidelity, Inc.
// Copyright 2020 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -54,6 +55,7 @@ namespace MenuOption {
const QString BookmarkAvatarEntities = "Bookmark Avatar Entities";
const QString BookmarkLocation = "Bookmark Location";
const QString CalibrateCamera = "Calibrate Camera";
const QString CachebustRequire = "Enable Cachebusting of Script.require";
const QString CenterPlayerInView = "Center Player In View";
const QString Chat = "Chat...";
const QString ClearDiskCaches = "Clear Disk Caches (requires restart)";

View file

@ -158,79 +158,98 @@ render::ItemID EntityTreeRenderer::renderableIdForEntityId(const EntityItemID& i
int EntityTreeRenderer::_entitiesScriptEngineCount = 0;
void EntityTreeRenderer::resetEntitiesScriptEngine() {
_entitiesScriptEngine = scriptEngineFactory(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
DependencyManager::get<ScriptEngines>()->runScriptInitializers(_entitiesScriptEngine);
_entitiesScriptEngine->runInThread();
auto entitiesScriptEngineProvider = qSharedPointerCast<EntitiesScriptEngineProvider>(_entitiesScriptEngine);
void EntityTreeRenderer::setupEntityScriptEngineSignals(const ScriptEnginePointer& scriptEngine) {
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
entityScriptingInterface->setEntitiesScriptEngine(entitiesScriptEngineProvider);
// Connect mouse events to entity script callbacks
if (!_mouseAndPreloadSignalHandlersConnected) {
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "mousePressOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseDoublePressOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "mouseDoublePressOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "mouseMoveOnEntity", event);
// FIXME: this is a duplicate of mouseMoveOnEntity, but it seems like some scripts might use this naming
_entitiesScriptEngine->callEntityScriptMethod(entityID, "mouseMoveEvent", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "mouseReleaseOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "mousePressOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseDoublePressOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "mouseDoublePressOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "mouseMoveOnEntity", event);
// FIXME: this is a duplicate of mouseMoveOnEntity, but it seems like some scripts might use this naming
scriptEngine->callEntityScriptMethod(entityID, "mouseMoveEvent", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "mouseReleaseOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "clickDownOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::holdingClickOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "holdingClickOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickReleaseOnEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "clickReleaseOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "clickDownOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::holdingClickOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "holdingClickOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickReleaseOnEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "clickReleaseOnEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "hoverEnterEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "hoverOverEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity, _entitiesScriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "hoverLeaveEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "hoverEnterEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "hoverOverEntity", event);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity, scriptEngine.data(), [&](const EntityItemID& entityID, const PointerEvent& event) {
scriptEngine->callEntityScriptMethod(entityID, "hoverLeaveEntity", event);
});
connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) {
EntityItemPointer entity = getTree()->findEntityByID(entityID);
if (entity) {
entity->setScriptHasFinishedPreload(true);
}
});
connect(scriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) {
EntityItemPointer entity = getTree()->findEntityByID(entityID);
if (entity) {
entity->setScriptHasFinishedPreload(true);
}
});
}
_mouseAndPreloadSignalHandlersConnected = true;
void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() {
if (_persistentEntitiesScriptEngine) {
_persistentEntitiesScriptEngine->unloadAllEntityScripts(true);
_persistentEntitiesScriptEngine->stop();
_persistentEntitiesScriptEngine->waitTillDoneRunning();
_persistentEntitiesScriptEngine->disconnectNonEssentialSignals();
}
_persistentEntitiesScriptEngine = scriptEngineFactory(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
DependencyManager::get<ScriptEngines>()->runScriptInitializers(_persistentEntitiesScriptEngine);
_persistentEntitiesScriptEngine->runInThread();
auto entitiesScriptEngineProvider = qSharedPointerCast<EntitiesScriptEngineProvider>(_persistentEntitiesScriptEngine);
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
entityScriptingInterface->setPersistentEntitiesScriptEngine(entitiesScriptEngineProvider);
setupEntityScriptEngineSignals(_persistentEntitiesScriptEngine);
}
void EntityTreeRenderer::resetNonPersistentEntitiesScriptEngine() {
if (_nonPersistentEntitiesScriptEngine) {
_nonPersistentEntitiesScriptEngine->unloadAllEntityScripts(false);
_nonPersistentEntitiesScriptEngine->stop();
_nonPersistentEntitiesScriptEngine->waitTillDoneRunning();
_nonPersistentEntitiesScriptEngine->disconnectNonEssentialSignals();
}
_nonPersistentEntitiesScriptEngine = scriptEngineFactory(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
DependencyManager::get<ScriptEngines>()->runScriptInitializers(_nonPersistentEntitiesScriptEngine);
_nonPersistentEntitiesScriptEngine->runInThread();
auto entitiesScriptEngineProvider = qSharedPointerCast<EntitiesScriptEngineProvider>(_nonPersistentEntitiesScriptEngine);
DependencyManager::get<EntityScriptingInterface>()->setNonPersistentEntitiesScriptEngine(entitiesScriptEngineProvider);
setupEntityScriptEngineSignals(_nonPersistentEntitiesScriptEngine);
}
void EntityTreeRenderer::stopDomainAndNonOwnedEntities() {
leaveDomainAndNonOwnedEntities();
// unload and stop the engine
if (_entitiesScriptEngine) {
QList<EntityItemID> entitiesWithEntityScripts = _entitiesScriptEngine->getListOfEntityScriptIDs();
if (_nonPersistentEntitiesScriptEngine) {
QList<EntityItemID> entitiesWithEntityScripts = _nonPersistentEntitiesScriptEngine->getListOfEntityScriptIDs();
foreach (const EntityItemID& entityID, entitiesWithEntityScripts) {
foreach (const EntityItemID& entityID, entitiesWithEntityScripts) {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (entityItem && !entityItem->getScript().isEmpty()) {
if (!(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) {
if (_currentEntitiesInside.contains(entityID)) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
_entitiesScriptEngine->unloadEntityScript(entityID, true);
_nonPersistentEntitiesScriptEngine->unloadEntityScript(entityID, true);
}
}
}
@ -240,6 +259,10 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() {
void EntityTreeRenderer::clearDomainAndNonOwnedEntities() {
stopDomainAndNonOwnedEntities();
if (!_shuttingDown && _wantScripts) {
resetNonPersistentEntitiesScriptEngine();
}
std::unordered_map<EntityItemID, EntityRendererPointer> savedEntities;
std::unordered_set<EntityRendererPointer> savedRenderables;
// remove all entities from the scene
@ -269,16 +292,22 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() {
void EntityTreeRenderer::clear() {
leaveAllEntities();
// unload and stop the engine
if (_entitiesScriptEngine) {
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
_entitiesScriptEngine->unloadAllEntityScripts(true);
_entitiesScriptEngine->stop();
}
// reset the engine
auto scene = _viewState->getMain3DScene();
if (_shuttingDown) {
// unload and stop the engines
if (_nonPersistentEntitiesScriptEngine) {
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
_nonPersistentEntitiesScriptEngine->unloadAllEntityScripts(true);
_nonPersistentEntitiesScriptEngine->stop();
}
if (_persistentEntitiesScriptEngine) {
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
_persistentEntitiesScriptEngine->unloadAllEntityScripts(true);
_persistentEntitiesScriptEngine->stop();
}
if (scene) {
render::Transaction transaction;
for (const auto& entry : _entitiesInScene) {
@ -289,7 +318,8 @@ void EntityTreeRenderer::clear() {
}
} else {
if (_wantScripts) {
resetEntitiesScriptEngine();
resetPersistentEntitiesScriptEngine();
resetNonPersistentEntitiesScriptEngine();
}
if (scene) {
for (const auto& entry : _entitiesInScene) {
@ -313,13 +343,17 @@ void EntityTreeRenderer::clear() {
}
void EntityTreeRenderer::reloadEntityScripts() {
_entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->resetModuleCache();
_persistentEntitiesScriptEngine->unloadAllEntityScripts();
_persistentEntitiesScriptEngine->resetModuleCache();
_nonPersistentEntitiesScriptEngine->unloadAllEntityScripts();
_nonPersistentEntitiesScriptEngine->resetModuleCache();
for (const auto& entry : _entitiesInScene) {
const auto& renderer = entry.second;
const auto& entity = renderer->getEntity();
if (!entity->getScript().isEmpty()) {
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true);
if (entity && !entity->getScript().isEmpty()) {
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
scriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true);
}
}
}
@ -329,7 +363,8 @@ void EntityTreeRenderer::init() {
EntityTreePointer entityTree = std::static_pointer_cast<EntityTree>(_tree);
if (_wantScripts) {
resetEntitiesScriptEngine();
resetPersistentEntitiesScriptEngine();
resetNonPersistentEntitiesScriptEngine();
}
forceRecheckEntities(); // setup our state to force checking our inside/outsideness of entities
@ -341,8 +376,11 @@ void EntityTreeRenderer::init() {
}
void EntityTreeRenderer::shutdown() {
if (_entitiesScriptEngine) {
_entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential
if (_persistentEntitiesScriptEngine) {
_persistentEntitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential
}
if (_nonPersistentEntitiesScriptEngine) {
_nonPersistentEntitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential
}
_shuttingDown = true;
@ -658,12 +696,16 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
// EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts
// for entity IDs that no longer exist.
if (_entitiesScriptEngine) {
if (_persistentEntitiesScriptEngine && _nonPersistentEntitiesScriptEngine) {
// for all of our previous containing entities, if they are no longer containing then send them a leave event
foreach(const EntityItemID& entityID, _currentEntitiesInside) {
if (!entitiesContainingAvatar.contains(entityID)) {
emit leaveEntity(entityID);
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
auto entity = getTree()->findEntityByEntityItemID(entityID);
if (entity) {
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
scriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
}
}
@ -671,7 +713,11 @@ void EntityTreeRenderer::checkEnterLeaveEntities() {
foreach(const EntityItemID& entityID, entitiesContainingAvatar) {
if (!_currentEntitiesInside.contains(entityID)) {
emit enterEntity(entityID);
_entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity");
auto entity = getTree()->findEntityByEntityItemID(entityID);
if (entity) {
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
scriptEngine->callEntityScriptMethod(entityID, "enterEntity");
}
}
}
_currentEntitiesInside = entitiesContainingAvatar;
@ -687,8 +733,8 @@ void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() {
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (entityItem && !(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) {
emit leaveEntity(entityID);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
if (_nonPersistentEntitiesScriptEngine) {
_nonPersistentEntitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
} else {
currentEntitiesInsideToSave.insert(entityID);
@ -706,8 +752,12 @@ void EntityTreeRenderer::leaveAllEntities() {
// for all of our previous containing entities, if they are no longer containing then send them a leave event
foreach(const EntityItemID& entityID, _currentEntitiesInside) {
emit leaveEntity(entityID);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID);
if (entityItem) {
auto& scriptEngine = (entityItem->isLocalEntity() || entityItem->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (scriptEngine) {
scriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
}
}
_currentEntitiesInside.clear();
@ -1003,11 +1053,12 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
return;
}
if (_tree && !_shuttingDown && _entitiesScriptEngine && !itr->second->getEntity()->getScript().isEmpty()) {
auto& scriptEngine = (itr->second->getEntity()->isLocalEntity() || itr->second->getEntity()->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (_tree && !_shuttingDown && scriptEngine && !itr->second->getEntity()->getScript().isEmpty()) {
if (_currentEntitiesInside.contains(entityID)) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
scriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
_entitiesScriptEngine->unloadEntityScript(entityID, true);
scriptEngine->unloadEntityScript(entityID, true);
}
auto scene = _viewState->getMain3DScene();
@ -1052,20 +1103,21 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool
if (!entity) {
return;
}
bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine;
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
bool shouldLoad = entity->shouldPreloadScript() && scriptEngine;
QString scriptUrl = entity->getScript();
if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) {
if (_entitiesScriptEngine) {
if (scriptEngine) {
if (_currentEntitiesInside.contains(entityID)) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
scriptEngine->callEntityScriptMethod(entityID, "leaveEntity");
}
_entitiesScriptEngine->unloadEntityScript(entityID);
scriptEngine->unloadEntityScript(entityID);
}
entity->scriptHasUnloaded();
}
if (shouldLoad) {
entity->setScriptHasFinishedPreload(false);
_entitiesScriptEngine->loadEntityScript(entityID, resolveScriptURL(scriptUrl), reload);
scriptEngine->loadEntityScript(entityID, resolveScriptURL(scriptUrl), reload);
entity->scriptHasPreloaded();
}
}
@ -1172,8 +1224,9 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons
if ((myNodeID == entityASimulatorID && entityAIsDynamic) || (myNodeID == entityBSimulatorID && (!entityAIsDynamic || entityASimulatorID.isNull()))) {
playEntityCollisionSound(entityA, collision);
emit collisionWithEntity(idA, idB, collision);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision);
auto& scriptEngine = (entityA->isLocalEntity() || entityA->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (scriptEngine) {
scriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision);
}
}
@ -1183,8 +1236,9 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons
Collision invertedCollision(collision);
invertedCollision.invert();
emit collisionWithEntity(idB, idA, invertedCollision);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision);
auto& scriptEngine = (entityB->isLocalEntity() || entityB->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (scriptEngine) {
scriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision);
}
}
}

View file

@ -173,7 +173,9 @@ private:
EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); }
render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); }
void resetEntitiesScriptEngine();
void resetPersistentEntitiesScriptEngine();
void resetNonPersistentEntitiesScriptEngine();
void setupEntityScriptEngineSignals(const ScriptEnginePointer& scriptEngine);
void findBestZoneAndMaybeContainingEntities(QSet<EntityItemID>& entitiesContainingAvatar);
@ -196,7 +198,8 @@ private:
QSet<EntityItemID> _currentEntitiesInside;
bool _wantScripts;
ScriptEnginePointer _entitiesScriptEngine;
ScriptEnginePointer _nonPersistentEntitiesScriptEngine; // used for domain + non-owned avatar entities, cleared on domain switch
ScriptEnginePointer _persistentEntitiesScriptEngine; // used for local + owned avatar entities, persists on domain switch, cleared on reload content
void playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision);
@ -214,8 +217,6 @@ private:
std::function<RayToEntityIntersectionResult(unsigned int)> _getPrevRayPickResultOperator;
std::function<void(unsigned int, bool)> _setPrecisionPickingOperator;
bool _mouseAndPreloadSignalHandlersConnected { false };
class LayeredZone {
public:
LayeredZone(std::shared_ptr<ZoneEntityItem> zone) : zone(zone), id(zone->getID()), volume(zone->getVolumeEstimate()) {}

View file

@ -1046,18 +1046,26 @@ QSizeF EntityScriptingInterface::textSize(const QUuid& id, const QString& text)
return EntityTree::textSize(id, text);
}
void EntityScriptingInterface::setEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine) {
void EntityScriptingInterface::setPersistentEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
_entitiesScriptEngine = engine;
_persistentEntitiesScriptEngine = engine;
}
void EntityScriptingInterface::setNonPersistentEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
_nonPersistentEntitiesScriptEngine = engine;
}
void EntityScriptingInterface::callEntityMethod(const QUuid& id, const QString& method, const QStringList& params) {
PROFILE_RANGE(script_entities, __FUNCTION__);
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
if (_entitiesScriptEngine) {
EntityItemID entityID{ id };
_entitiesScriptEngine->callEntityScriptMethod(entityID, method, params);
auto entity = getEntityTree()->findEntityByEntityItemID(id);
if (entity) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (scriptEngine) {
scriptEngine->callEntityScriptMethod(id, method, params);
}
}
}
@ -1099,9 +1107,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer
params << paramString;
}
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(entityID, method, params, senderNode->getUUID());
auto entity = getEntityTree()->findEntityByEntityItemID(entityID);
if (entity) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine;
if (scriptEngine) {
scriptEngine->callEntityScriptMethod(entityID, method, params, senderNode->getUUID());
}
}
}
}
@ -1332,7 +1344,7 @@ bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue h
if (entitiesScriptEngine) {
request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
}
});
}, entityID);
if (!request->isStarted()) {
request->deleteLater();
callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue());

View file

@ -181,7 +181,8 @@ public:
void setEntityTree(EntityTreePointer modelTree);
EntityTreePointer getEntityTree() { return _entityTree; }
void setEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine);
void setPersistentEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine);
void setNonPersistentEntitiesScriptEngine(QSharedPointer<EntitiesScriptEngineProvider> engine);
void resetActivityTracking();
ActivityTracking getActivityTracking() const { return _activityTracking; }
@ -2510,9 +2511,12 @@ signals:
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message);
protected:
void withEntitiesScriptEngine(std::function<void(QSharedPointer<EntitiesScriptEngineProvider>)> function) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
function(_entitiesScriptEngine);
void withEntitiesScriptEngine(std::function<void(QSharedPointer<EntitiesScriptEngineProvider>)> function, const EntityItemID& id) {
auto entity = getEntityTree()->findEntityByEntityItemID(id);
if (entity) {
std::lock_guard<std::recursive_mutex> lock(_entitiesScriptEngineLock);
function((entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine);
}
};
private slots:
@ -2542,7 +2546,8 @@ private:
EntityTreePointer _entityTree;
std::recursive_mutex _entitiesScriptEngineLock;
QSharedPointer<EntitiesScriptEngineProvider> _entitiesScriptEngine;
QSharedPointer<EntitiesScriptEngineProvider> _persistentEntitiesScriptEngine;
QSharedPointer<EntitiesScriptEngineProvider> _nonPersistentEntitiesScriptEngine;
bool _bidOnSimulationOwnership { false };

View file

@ -13,6 +13,7 @@
#include <QtCore/QFile>
#include <QtCore/QFileSelector>
#include <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <StatTracker.h>
@ -54,6 +55,7 @@ void FileResourceRequest::doSend() {
} else {
QFile file(filename);
if (file.exists()) {
setProperty("last-modified", toHttpDateString(QFileInfo(file).lastModified().toMSecsSinceEpoch()));
if (file.open(QFile::ReadOnly)) {
if (file.size() < _byteRange.fromInclusive || file.size() < _byteRange.toExclusive) {

View file

@ -15,8 +15,14 @@
#include <DependencyManager.h>
#include <StatTracker.h>
#include <QtCore/QDateTime>
#include <QtCore/QThread>
QString ResourceRequest::toHttpDateString(uint64_t msecsSinceEpoch) {
return QDateTime::fromMSecsSinceEpoch(msecsSinceEpoch)
.toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'")
.toLatin1();
}
void ResourceRequest::send() {
if (QThread::currentThread() != thread()) {

View file

@ -90,6 +90,7 @@ public:
void setCacheEnabled(bool value) { _cacheEnabled = value; }
void setByteRange(ByteRange byteRange) { _byteRange = byteRange; }
static QString toHttpDateString(uint64_t msecsSinceEpoch);
public slots:
void send();

View file

@ -85,11 +85,25 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
Lock lock(_containerLock);
if (_scriptCache.contains(url) && !forceDownload) {
auto scriptContent = _scriptCache[url];
lock.unlock();
qCDebug(scriptengine) << "Found script in cache:" << url.fileName();
contentAvailable(url.toString(), scriptContent, true, true, STATUS_CACHED);
} else {
auto entry = _scriptCache[url];
if (url.isLocalFile() || url.scheme().isEmpty()) {
auto modifiedTime = QFileInfo(url.toLocalFile()).lastModified();
QString localTime = ResourceRequest::toHttpDateString(modifiedTime.toMSecsSinceEpoch());
QString cachedTime = entry["last-modified"].toString();
if (cachedTime != localTime) {
forceDownload = true;
qCDebug(scriptengine) << "Found script in cache, but local file modified; reloading:" << url.fileName()
<< "(memory:" << cachedTime << "disk:" << localTime << ")";
}
}
if (!forceDownload) {
lock.unlock();
qCDebug(scriptengine) << "Found script in cache:" << url.fileName();
contentAvailable(url.toString(), entry["data"].toString(), true, true, STATUS_CACHED);
return;
}
}
{
auto& scriptRequest = _activeScriptRequests[url];
bool alreadyWaiting = scriptRequest.scriptUsers.size() > 0;
scriptRequest.scriptUsers.push_back(contentAvailable);
@ -140,7 +154,10 @@ void ScriptCache::scriptContentAvailable(int maxRetries) {
_activeScriptRequests.remove(url);
_scriptCache[url] = scriptContent = req->getData();
_scriptCache[url] = {
{ "data", scriptContent = req->getData() },
{ "last-modified", req->property("last-modified") },
};
} else {
auto result = req->getResult();
bool irrecoverable =
@ -177,7 +194,7 @@ void ScriptCache::scriptContentAvailable(int maxRetries) {
allCallbacks = scriptRequest.scriptUsers;
if (_scriptCache.contains(url)) {
scriptContent = _scriptCache[url];
scriptContent = _scriptCache[url]["data"].toString();
}
_activeScriptRequests.remove(url);
qCWarning(scriptengine) << "Error loading script from URL (" << status <<")";

View file

@ -60,7 +60,7 @@ private:
Mutex _containerLock;
QMap<QUrl, ScriptRequest> _activeScriptRequests;
QHash<QUrl, QString> _scriptCache;
QHash<QUrl, QVariantMap> _scriptCache;
QMultiMap<QUrl, ScriptUser*> _scriptUsers;
};

View file

@ -1941,9 +1941,12 @@ QScriptValue ScriptEngine::require(const QString& moduleId) {
// modules get cached in `Script.require.cache` and (similar to Node.js) users can access it
// to inspect particular entries and invalidate them by deleting the key:
// `delete Script.require.cache[Script.require.resolve(moduleId)];`
// Check to see if we should invalidate the cache based on a user setting.
Setting::Handle<bool> getCachebustSetting {"cachebustScriptRequire", false };
// cacheMeta is just used right now to tell deleted keys apart from undefined ones
bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid();
bool invalidateCache = getCachebustSetting.get() || (module.isUndefined() && cacheMeta.property(moduleId).isValid());
// reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load
cacheMeta.setProperty(modulePath, QScriptValue());

View file

@ -4,6 +4,7 @@
//
// Created by Brad Hefta-Gaub on 12/14/13.
// Copyright 2013 High Fidelity, Inc.
// Copyright 2020 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html

View file

@ -53,5 +53,5 @@ set(DIR "opusCodec")
add_subdirectory(${DIR})
# example plugins
set(DIR "KasenAPIExample")
set(DIR "JSAPIExample")
add_subdirectory(${DIR})

View file

@ -0,0 +1,3 @@
set(TARGET_NAME JSAPIExample)
setup_hifi_client_server_plugin(scripting)
link_hifi_libraries(shared plugins)

View file

@ -0,0 +1,251 @@
//
// JSAPIExample.cpp
// plugins/JSAPIExample/src
//
// Copyright (c) 2019-2020 humbletim (humbletim@gmail.com)
// Copyright (c) 2019 Kalila L. (kasenvr@gmail.com)
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// Example of prototyping new JS APIs by leveraging the existing plugin system.
#include <QCoreApplication>
#include <QtCore/QByteArray>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QJsonObject>
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#include <QtScript/QScriptEngine>
#include <QtScript/QScriptable>
#include <SettingHelpers.h> // for ::settingsFilename()
#include <SharedUtil.h> // for ::usecTimestampNow()
#include <shared/ScriptInitializerMixin.h>
// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes)
namespace REPLACE_ME_WITH_UNIQUE_NAME {
static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1";
static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample";
QLoggingCategory logger { "jsapiexample" };
inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) {
if (context) {
// when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side)
context->throwError(message);
} else {
// otherwise just log the error
qCWarning(logger) << "error:" << message;
}
return returnValue;
}
QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
class JSAPIExample : public QObject, public QScriptable {
Q_OBJECT
Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json")
Q_PROPERTY(QString version MEMBER _version CONSTANT)
public:
JSAPIExample() {
setObjectName(JSAPI_EXPORT_NAME);
auto scriptInit = DependencyManager::get<ScriptInitializers>();
if (!scriptInit) {
qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
return;
}
qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data();
scriptInit->registerScriptInitializer([this](QScriptEngine* engine) {
auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater);
engine->globalObject().setProperty(objectName(), value);
// qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName");
});
// qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
}
// NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject
// also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples --
// which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs.
// (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine)
public slots:
// pretty-printed representation for logging eg: print(JSAPIExample)
// (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings)
QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }
/**jsdoc
* Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
* @example <caption>Measure current setTimeout accuracy.</caption>
* var expected = 1000;
* var start = JSAPIExample.now();
* Script.setTimeout(function () {
* var elapsed = (JSAPIExample.now() - start)/1000;
* print("expected (ms):", expected, "actual (ms):", elapsed);
* }, expected);
*/
QVariant now() const { return usecTimestampNow(); }
/**jsdoc
* Example of returning a JS Object key-value map
* @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption>
* print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 }
*/
QVariant zip(const QStringList& keys, const QVariantList& values) const {
QVariantMap out;
for (int i = 0; i < keys.size(); i++) {
out[keys[i]] = i < values.size() ? values[i] : QVariant();
}
return out;
}
/**jsdoc
* Example of returning a JS Array result
* @example <caption>emulate Object.values(keyValues)</caption>
* print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2]
*/
QVariant values(const QVariantMap& keyValues) const {
QVariantList values = keyValues.values();
return values;
}
/**jsdoc
* Another example of returning JS Array data
* @example <caption>generate an integer sequence (inclusive of [from, to])</caption>
* print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5]
*/
QVariant seq(int from, int to) const {
QVariantList out;
for (int i = from; i <= to; i++) {
out.append(i);
}
return out;
}
/**jsdoc
* Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer)
* see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples
* @example <caption>return compressed/decompressed versions of the input data</caption>
* var data = "testing 1 2 3";
* var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer
* var u = JSAPIExample.qUncompressString(z); // u will be a String value
* print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length }));
*/
QVariant qCompressString(const QString& jsString, int compress_level = -1) const {
QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level);
return arrayBuffer;
}
QVariant qUncompressString(const QByteArray& arrayBuffer) const {
QString jsString = QString::fromUtf8(qUncompress(arrayBuffer));
return jsString;
}
/**
* Example of exposing a custom "managed" C++ QObject to JS
* The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine --
* it will be automatically cleaned up once no longer reachable from any JS variables/closures.
* @example <caption>access persistent settings stored in separate .json files</caption>
* var settings = JSAPIExample.getScopedSettings("example");
* print("example settings stored in:", settings.fileName());
* print("(before) example::timestamp", settings.getValue("timestamp"));
* settings.setValue("timestamp", Date.now());
* print("(after) example::timestamp", settings.getValue("timestamp"));
* print("all example::* keys", settings.allKeys());
* settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector
*/
QScriptValue getScopedSettings(const QString& scope) {
auto engine = QScriptable::engine();
if (!engine) {
return QScriptValue::NullValue;
}
QString error;
auto cppValue = createScopedSettings(scope, engine, error);
if (!cppValue) {
raiseScriptingError(context(), "error creating scoped settings instance: " + error);
return QScriptValue::NullValue;
}
return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater);
}
private:
const QString _version { JSAPI_SEMANTIC_VERSION };
};
// JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots
class JSSettingsHelper : public QObject {
Q_OBJECT
public:
JSSettingsHelper(const QString& scope, QObject* parent = nullptr);
~JSSettingsHelper();
operator bool() const;
public slots:
QString fileName() const;
QString toString() const;
QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant());
bool setValue(const QString& key, const QVariant& value);
QStringList allKeys() const;
protected:
QString _scope;
QString _fileName;
QSharedPointer<QSettings> _settings;
QString getLocalSettingsPath(const QString& scope) const;
};
// verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance
QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) {
const QRegExp VALID_SETTINGS_SCOPE { "[-_A-Za-z0-9]{1,64}" };
if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) {
error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope);
return nullptr;
}
return new JSSettingsHelper(scope, parent);
}
// --------------------------------------------------
// ----- inline JSSettingsHelper implementation -----
JSSettingsHelper::operator bool() const {
return (bool)_settings;
}
JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) :
QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)),
_settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {
}
JSSettingsHelper::~JSSettingsHelper() {
qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this;
}
QString JSSettingsHelper::fileName() const {
return _settings ? _settings->fileName() : "";
}
QString JSSettingsHelper::toString() const {
return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings);
}
QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) {
return _settings ? _settings->value(key, defaultValue) : defaultValue;
}
bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) {
if (_settings) {
if (value.isValid()) {
_settings->setValue(key, value);
} else {
_settings->remove(key);
}
return true;
}
return false;
}
QStringList JSSettingsHelper::allKeys() const {
return _settings ? _settings->allKeys() : QStringList{};
}
QString JSSettingsHelper::getLocalSettingsPath(const QString& scope) const {
// generate a prefixed filename (relative to the main application's Interface.json file)
const QString fileName = QString("jsapi_%1.json").arg(scope);
return QFileInfo(::settingsFilename()).dir().filePath(fileName);
}
// ----- /inline JSSettingsHelper implementation -----
} // namespace REPLACE_ME_WITH_UNIQUE_NAME
#include "JSAPIExample.moc"

View file

@ -0,0 +1,4 @@
{
"name":"JS API Example",
"version": 1
}

View file

@ -1,3 +0,0 @@
set(TARGET_NAME KasenAPIExample)
setup_hifi_client_server_plugin(scripting)
link_hifi_libraries(shared plugins avatars networking graphics gpu)

View file

@ -1,58 +0,0 @@
//
// ExampleScriptPlugin.h
// plugins/KasenAPIExample/src
//
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
// Copyright 2019 Kasen IO
//
// Authored by: Humbletim (humbletim@gmail.com)
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// Supporting file containing all QtScript specific integration.
#ifndef EXAMPLE_SCRIPT_PLUGIN_H
#define EXAMPLE_SCRIPT_PLUGIN_H
#if DEV_BUILD
#pragma message("QtScript is deprecated see: doc.qt.io/qt-5/topics-scripting.html")
#endif
#include <QtScript/QScriptEngine>
#include <QtCore/QLoggingCategory>
#include <QCoreApplication>
#include <shared/ScriptInitializerMixin.h>
namespace example {
extern const QLoggingCategory& logger;
inline void setGlobalInstance(QScriptEngine* engine, const QString& name, QObject* object) {
auto value = engine->newQObject(object, QScriptEngine::QtOwnership);
engine->globalObject().setProperty(name, value);
qCDebug(logger) << "setGlobalInstance" << name << engine->property("fileName");
}
class ScriptPlugin : public QObject {
Q_OBJECT
QString _version;
Q_PROPERTY(QString version MEMBER _version CONSTANT)
protected:
inline ScriptPlugin(const QString& name, const QString& version) : _version(version) {
setObjectName(name);
if (!DependencyManager::get<ScriptInitializers>()) {
qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
return;
}
qCWarning(logger) << "registering w/ScriptInitializerMixin..." << DependencyManager::get<ScriptInitializers>().data();
DependencyManager::get<ScriptInitializers>()->registerScriptInitializer(
[this](QScriptEngine* engine) { setGlobalInstance(engine, objectName(), this); });
}
public slots:
inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }
};
} // namespace example
#endif

View file

@ -1,141 +0,0 @@
//
// KasenAPIExample.cpp
// plugins/KasenAPIExample/src
//
// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com
// Copyright 2019 Kasen IO
//
// Authored by: Humbletim (humbletim@gmail.com)
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// Example of prototyping new JS APIs by leveraging the existing plugin system.
#include "ExampleScriptPlugin.h"
#include <QCoreApplication>
#include <QtCore/QJsonObject>
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#include <SharedUtil.h>
#include <AvatarHashMap.h>
namespace custom_api_example {
QLoggingCategory logger{ "custom_api_example" };
class KasenAPIExample : public example::ScriptPlugin {
Q_OBJECT
Q_PLUGIN_METADATA(IID "KasenAPIExample" FILE "plugin.json")
public:
KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") {
qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
}
public slots:
/**jsdoc
* Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
* @example <caption>Measure current setTimeout accuracy.</caption>
* var expected = 1000;
* var start = KasenAPIExample.now();
* Script.setTimeout(function () {
* var elapsed = (KasenAPIExample.now() - start)/1000;
* print("expected (ms):", expected, "actual (ms):", elapsed);
* }, expected);
*/
QVariant now() const {
return usecTimestampNow();
}
/**jsdoc
* Returns the available blendshape names for an avatar.
* @example <caption>Get blendshape names</caption>
* print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID)));
*/
QStringList getBlendshapeNames(const QUuid& avatarID) const {
QVector<QString> out;
if (auto head = getAvatarHead(avatarID)) {
for (const auto& kv : head->getBlendshapeMap().toStdMap()) {
if (kv.second >= out.size()) out.resize(kv.second+1);
out[kv.second] = kv.first;
}
}
return out.toList();
}
/**jsdoc
* Returns a key-value object with active (non-zero) blendshapes.
* eg: { JawOpen: 1.0, ... }
* @example <caption>Get active blendshape map</caption>
* print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID)));
*/
QVariant getActiveBlendshapes(const QUuid& avatarID) const {
if (auto head = getAvatarHead(avatarID)) {
return head->toJson()["blendShapes"].toVariant();
}
return {};
}
QVariant getBlendshapeMapping(const QUuid& avatarID) const {
QVariantMap out;
if (auto head = getAvatarHead(avatarID)) {
for (const auto& kv : head->getBlendshapeMap().toStdMap()) {
out[kv.first] = kv.second;
}
}
return out;
}
QVariant getBlendshapes(const QUuid& avatarID) const {
QVariantMap result;
if (auto head = getAvatarHead(avatarID)) {
QStringList names = getBlendshapeNames(avatarID);
auto states = head->getBlendshapeStates();
result = {
{ "base", zipNonZeroValues(names, states.base) },
{ "summed", zipNonZeroValues(names, states.summed) },
{ "transient", zipNonZeroValues(names, states.transient) },
};
}
return result;
}
private:
static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector<float>& values) {
QVariantMap out;
for (int i=1; i < values.size(); i++) {
if (fabs(values[i]) > 1.0e-6f) {
out[keys.value(i)] = values[i];
}
}
return out;
}
struct _HeadHelper : public HeadData {
QMap<QString,int> getBlendshapeMap() const {
return BLENDSHAPE_LOOKUP_MAP;
}
struct States { QVector<float> base, summed, transient; };
States getBlendshapeStates() const {
return {
_blendshapeCoefficients,
_summedBlendshapeCoefficients,
_transientBlendshapeCoefficients
};
}
};
static const _HeadHelper* getAvatarHead(const QUuid& avatarID) {
auto avatars = DependencyManager::get<AvatarHashMap>();
auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr;
auto head = avatar ? avatar->getHeadData() : nullptr;
return reinterpret_cast<const _HeadHelper*>(head);
}
};
}
const QLoggingCategory& example::logger{ custom_api_example::logger };
#include "KasenAPIExample.moc"

View file

@ -1,21 +0,0 @@
{
"name":"Kasen JS API Example",
"version": 1,
"package": {
"author": "Revofire",
"homepage": "www.realities.dev",
"version": "0.0.1",
"engines": {
"hifi-interface": ">= 0.83.0",
"hifi-assignment-client": ">= 0.83.0"
},
"config": {
"client": true,
"entity_client": true,
"entity_server": true,
"edit_filter": true,
"agent": true,
"avatar": true
}
}
}

View file

@ -146,20 +146,23 @@ var DEFAULT_DIMENSIONS = {
var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS);
var SUBMENU_ENTITY_EDITOR_PREFERENCES = "Edit > Create Application - Preferences";
var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select";
var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus";
var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Create Mode";
var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Create Mode";
var MENU_CREATE_ENTITIES_GRABBABLE = "Create Entities As Grabbable (except Zones, Particles, and Lights)";
var MENU_ALLOW_SELECTION_LARGE = "Allow Selecting of Large Models";
var MENU_ALLOW_SELECTION_SMALL = "Allow Selecting of Small Models";
var MENU_ALLOW_SELECTION_LIGHTS = "Allow Selecting of Lights";
var MENU_ENTITY_LIST_DEFAULT_RADIUS = "Entity List Default Radius";
var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect";
var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus";
var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode";
var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode";
var SETTING_EDITOR_COLUMNS_SETUP = "editorColumnsSetup";
var SETTING_ENTITY_LIST_DEFAULT_RADIUS = "entityListDefaultRadius";
var SETTING_EDIT_PREFIX = "Edit/";
@ -267,8 +270,6 @@ function adjustPositionPerBoundingBox(position, direction, registration, dimensi
return position;
}
var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit";
// Handles any edit mode updates required when domains have switched
function checkEditPermissionsAndUpdate() {
if ((createButton === null) || (createButton === undefined)) {
@ -878,7 +879,12 @@ var toolBar = (function () {
addButton("importEntitiesButton", function() {
Window.browseChanged.connect(onFileOpenChanged);
Window.browseAsync("Select Model to Import", "", "*.json");
Window.browseAsync("Select .json to Import", "", "*.json");
});
addButton("importEntitiesFromUrlButton", function() {
Window.promptTextChanged.connect(onPromptTextChanged);
Window.promptAsync("URL of a .json to import", "");
});
addButton("openAssetBrowserButton", function() {
@ -1378,11 +1384,9 @@ Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
// In order for editVoxels and editModels to play nice together, they each check to see if a "delete" menu item already
// exists. If it doesn't they add it. If it does they don't. They also only delete the menu item if they were the one that
// added it.
var modelMenuAddedDelete = false;
var originalLightsArePickable = Entities.getLightsArePickable();
function setupModelMenus() {
// adj our menuitems
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Undo",
@ -1396,118 +1400,69 @@ function setupModelMenus() {
position: 1,
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Entities",
isSeparator: true
});
if (!Menu.menuItemExists("Edit", "Delete")) {
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Delete",
shortcutKeyEvent: {
text: "delete"
},
afterItem: "Entities",
});
modelMenuAddedDelete = true;
}
Menu.addMenu(SUBMENU_ENTITY_EDITOR_PREFERENCES);
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Parent Entity to Last",
afterItem: "Entities"
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Unparent Entity",
afterItem: "Parent Entity to Last"
});
Menu.addMenuItem({
menuName: GRABBABLE_ENTITIES_MENU_CATEGORY,
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_CREATE_ENTITIES_GRABBABLE,
afterItem: "Unparent Entity",
position: 0,
isCheckable: true,
isChecked: Settings.getValue(SETTING_EDIT_PREFIX + MENU_CREATE_ENTITIES_GRABBABLE, false)
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_ALLOW_SELECTION_LARGE,
afterItem: MENU_CREATE_ENTITIES_GRABBABLE,
isCheckable: true,
isChecked: Settings.getValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_LARGE, true)
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_ALLOW_SELECTION_SMALL,
afterItem: MENU_ALLOW_SELECTION_LARGE,
isCheckable: true,
isChecked: Settings.getValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_SMALL, true)
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_ALLOW_SELECTION_LIGHTS,
afterItem: MENU_ALLOW_SELECTION_SMALL,
isCheckable: true,
isChecked: Settings.getValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_LIGHTS, false)
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Select All Entities In Box",
afterItem: "Allow Selecting of Lights"
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Select All Entities Touching Box",
afterItem: "Select All Entities In Box"
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Export Entities",
afterItem: "Entities"
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Import Entities",
afterItem: "Export Entities"
});
Menu.addMenuItem({
menuName: "Edit",
menuItemName: "Import Entities from URL",
afterItem: "Import Entities"
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_AUTO_FOCUS_ON_SELECT,
afterItem: MENU_ALLOW_SELECTION_LIGHTS,
isCheckable: true,
isChecked: Settings.getValue(SETTING_AUTO_FOCUS_ON_SELECT) === "true"
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_EASE_ON_FOCUS,
afterItem: MENU_AUTO_FOCUS_ON_SELECT,
isCheckable: true,
isChecked: Settings.getValue(SETTING_EASE_ON_FOCUS) === "true"
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE,
afterItem: MENU_EASE_ON_FOCUS,
isCheckable: true,
isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false"
});
Menu.addMenuItem({
menuName: "Edit",
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE,
afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE,
isCheckable: true,
isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false"
});
Menu.addMenuItem({
menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES,
menuItemName: MENU_ENTITY_LIST_DEFAULT_RADIUS,
afterItem: MENU_SHOW_ZONES_IN_EDIT_MODE
});
Entities.setLightsArePickable(false);
}
@ -1518,29 +1473,16 @@ function cleanupModelMenus() {
Menu.removeMenuItem("Edit", "Undo");
Menu.removeMenuItem("Edit", "Redo");
Menu.removeSeparator("Edit", "Entities");
if (modelMenuAddedDelete) {
// delete our menuitems
Menu.removeMenuItem("Edit", "Delete");
}
Menu.removeMenuItem("Edit", "Parent Entity to Last");
Menu.removeMenuItem("Edit", "Unparent Entity");
Menu.removeMenuItem("Edit", "Allow Selecting of Large Models");
Menu.removeMenuItem("Edit", "Allow Selecting of Small Models");
Menu.removeMenuItem("Edit", "Allow Selecting of Lights");
Menu.removeMenuItem("Edit", "Select All Entities In Box");
Menu.removeMenuItem("Edit", "Select All Entities Touching Box");
Menu.removeMenuItem("Edit", "Export Entities");
Menu.removeMenuItem("Edit", "Import Entities");
Menu.removeMenuItem("Edit", "Import Entities from URL");
Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT);
Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS);
Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE);
Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE);
Menu.removeMenuItem("Edit", MENU_CREATE_ENTITIES_GRABBABLE);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_ALLOW_SELECTION_LARGE);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_ALLOW_SELECTION_SMALL);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_ALLOW_SELECTION_LIGHTS);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_AUTO_FOCUS_ON_SELECT);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_EASE_ON_FOCUS);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_SHOW_ZONES_IN_EDIT_MODE);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_CREATE_ENTITIES_GRABBABLE);
Menu.removeMenuItem(SUBMENU_ENTITY_EDITOR_PREFERENCES, MENU_ENTITY_LIST_DEFAULT_RADIUS);
Menu.removeMenu(SUBMENU_ENTITY_EDITOR_PREFERENCES);
}
Script.scriptEnding.connect(function () {
@ -1881,48 +1823,39 @@ function onPromptTextChanged(prompt) {
}
}
function onPromptTextChangedDefaultRadiusUserPref(prompt) {
Window.promptTextChanged.disconnect(onPromptTextChangedDefaultRadiusUserPref);
if (prompt !== "") {
var radius = parseInt(prompt);
if (radius < 0 || isNaN(radius)){
radius = 100;
}
Settings.setValue(SETTING_ENTITY_LIST_DEFAULT_RADIUS, radius);
}
}
function handleMenuEvent(menuItem) {
if (menuItem === "Allow Selecting of Small Models") {
allowSmallModels = Menu.isOptionChecked("Allow Selecting of Small Models");
} else if (menuItem === "Allow Selecting of Large Models") {
allowLargeModels = Menu.isOptionChecked("Allow Selecting of Large Models");
} else if (menuItem === "Allow Selecting of Lights") {
Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights"));
if (menuItem === MENU_ALLOW_SELECTION_SMALL) {
allowSmallModels = Menu.isOptionChecked(MENU_ALLOW_SELECTION_SMALL);
} else if (menuItem === MENU_ALLOW_SELECTION_LARGE) {
allowLargeModels = Menu.isOptionChecked(MENU_ALLOW_SELECTION_LARGE);
} else if (menuItem === MENU_ALLOW_SELECTION_LIGHTS) {
Entities.setLightsArePickable(Menu.isOptionChecked(MENU_ALLOW_SELECTION_LIGHTS));
} else if (menuItem === "Delete") {
deleteSelectedEntities();
} else if (menuItem === "Undo") {
undoHistory.undo();
} else if (menuItem === "Redo") {
undoHistory.redo();
} else if (menuItem === "Parent Entity to Last") {
parentSelectedEntities();
} else if (menuItem === "Unparent Entity") {
unparentSelectedEntities();
} else if (menuItem === "Export Entities") {
if (!selectionManager.hasSelection()) {
Window.notifyEditError("No entities have been selected.");
} else {
Window.saveFileChanged.connect(onFileSaveChanged);
Window.saveAsync("Select Where to Save", "", "*.json");
}
} else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") {
if (menuItem === "Import Entities") {
Window.browseChanged.connect(onFileOpenChanged);
Window.browseAsync("Select Model to Import", "", "*.json");
} else {
Window.promptTextChanged.connect(onPromptTextChanged);
Window.promptAsync("URL of SVO to import", "");
}
} else if (menuItem === "Select All Entities In Box") {
selectAllEntitiesInCurrentSelectionBox(false);
} else if (menuItem === "Select All Entities Touching Box") {
selectAllEntitiesInCurrentSelectionBox(true);
} else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) {
entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE));
} else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) {
Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE));
} else if (menuItem === MENU_CREATE_ENTITIES_GRABBABLE) {
Settings.setValue(SETTING_EDIT_PREFIX + menuItem, Menu.isOptionChecked(menuItem));
} else if (menuItem === MENU_ENTITY_LIST_DEFAULT_RADIUS) {
Window.promptTextChanged.connect(onPromptTextChangedDefaultRadiusUserPref);
Window.promptAsync("Entity List Default Radius (in meters)", "" + Settings.getValue(SETTING_ENTITY_LIST_DEFAULT_RADIUS, 100));
}
tooltip.show(false);
}

View file

@ -371,6 +371,16 @@ EntityListTool = function(shouldUseEditTabletApp) {
SelectionManager.teleportToEntity();
} else if (data.type === 'moveEntitySelectionToAvatar') {
SelectionManager.moveEntitiesSelectionToAvatar();
} else if (data.type === 'loadConfigSetting') {
var columnsData = Settings.getValue(SETTING_EDITOR_COLUMNS_SETUP, "NO_DATA");
var defaultRadius = Settings.getValue(SETTING_ENTITY_LIST_DEFAULT_RADIUS, 100);
emitJSONScriptEvent({
"type": "loadedConfigSetting",
"columnsData": columnsData,
"defaultRadius": defaultRadius
});
} else if (data.type === 'saveColumnsConfigSetting') {
Settings.setValue(SETTING_EDITOR_COLUMNS_SETUP, data.columnsData);
}
};

View file

@ -154,7 +154,7 @@
<div class = "menu-item-caption">Move Selected Entities to Avatar</div>
<div class = "menu-item-shortcut"></div>
</div>
</button>
</button>
</div>
<div class="entity-list-menu" id="selection-menu" >
<button class="menu-button" id="selectall" >
@ -209,23 +209,23 @@
</button>
<button class="menu-button" id="selectfamily" >
<div class = "menu-item">
<div class = "menu-item-caption">Select Family</div>
<div class = "menu-item-caption">Select Parent And All Its Children</div>
<div class = "menu-item-shortcut"></div>
</div>
</button>
<button class="menu-button" id="selecttopfamily" >
<div class = "menu-item">
<div class = "menu-item-caption">Select Top Family</div>
<div class = "menu-item-caption">Select Top Parent And All Its Children</div>
<div class = "menu-item-shortcut"></div>
</div>
</button>
</button>
<div class="menu-separator"></div>
<button class="menu-button" id="teleport-to-entity" >
<div class = "menu-item">
<div class = "menu-item-caption">Teleport To Selected Entities</div>
<div class = "menu-item-shortcut"></div>
</div>
</button>
</button>
</div>
<div id="menuBackgroundOverlay" ></div>
</body>

View file

@ -20,7 +20,7 @@ const EMPTY_ENTITY_ID = "0";
const MAX_LENGTH_RADIUS = 9;
const MINIMUM_COLUMN_WIDTH = 24;
const SCROLLBAR_WIDTH = 20;
const RESIZER_WIDTH = 10;
const RESIZER_WIDTH = 13; //Must be the number of COLUMNS - 1.
const DELTA_X_MOVE_COLUMNS_THRESHOLD = 2;
const DELTA_X_COLUMN_SWAP_POSITION = 5;
const CERTIFIED_PLACEHOLDER = "** Certified **";
@ -188,6 +188,8 @@ let selectedEntities = [];
let entityList = null; // The ListView
let hmdMultiSelectMode = false;
let lastSelectedEntity;
/**
* @type EntityListContextMenu
*/
@ -283,6 +285,9 @@ const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) {
function loaded() {
openEventBridge(function() {
var isColumnsSettingLoaded = false;
elEntityTable = document.getElementById("entity-table");
elEntityTableHeader = document.getElementById("entity-table-header");
elEntityTableBody = document.getElementById("entity-table-body");
@ -331,7 +336,7 @@ function loaded() {
elColumnsMultiselectBox = document.getElementById("entity-table-columns-multiselect-box");
elColumnsOptions = document.getElementById("entity-table-columns-options");
elToggleSpaceMode = document.getElementById('toggle-space-mode');
document.body.onclick = onBodyClick;
elToggleLocked.onclick = function() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
@ -618,9 +623,9 @@ function loaded() {
++columnIndex;
}
elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th");
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow,
clearRow, preRefresh, postRefresh, preRefresh, WINDOW_NONVARIABLE_HEIGHT);
@ -765,10 +770,10 @@ function loaded() {
let selectedIndex = selectedEntities.indexOf(entityID);
if (selectedIndex >= 0) {
selection = [];
selection = selection.concat(selectedEntities);
selection = selectedEntities.concat(selection);
selection.splice(selectedIndex, 1);
} else {
selection = selection.concat(selectedEntities);
selection = selectedEntities.concat(selection);
}
} else if (clickEvent.shiftKey && selectedEntities.length > 0) {
let previousItemFound = -1;
@ -1044,6 +1049,8 @@ function loaded() {
function updateSelectedEntities(selectedIDs, autoScroll) {
let notFound = false;
lastSelectedEntity = selectedIDs[selectedIDs.length - 1];
// reset all currently selected entities and their rows first
selectedEntities.forEach(function(id) {
let entity = entitiesByID[id];
@ -1063,7 +1070,11 @@ function loaded() {
if (entity !== undefined) {
entity.selected = true;
if (entity.elRow) {
entity.elRow.className = 'selected';
if (id === lastSelectedEntity) {
entity.elRow.className = 'last-selected';
} else {
entity.elRow.className = 'selected';
}
}
} else {
notFound = true;
@ -1132,7 +1143,11 @@ function loaded() {
// if this entity was previously selected flag it's row as selected
if (itemData.selected) {
elRow.className = 'selected';
if (itemData.id === lastSelectedEntity) {
elRow.className = 'last-selected';
} else {
elRow.className = 'selected';
}
} else {
elRow.className = '';
}
@ -1409,6 +1424,10 @@ function loaded() {
column.elResizer.style.visibility = columnVisible && visibleColumns > 0 ? "visible" : "hidden";
}
if (isColumnsSettingLoaded) {
EventBridge.emitWebEvent(JSON.stringify({ type: 'saveColumnsConfigSetting', columnsData: columns }));
}
entityList.refresh();
}
@ -1660,14 +1679,63 @@ function loaded() {
} else {
document.getElementById("hmdmultiselect").style.display = "none";
}
} else if (data.type === "loadedConfigSetting") {
if (typeof(data.defaultRadius) === "number") {
elFilterRadius.value = data.defaultRadius;
onRadiusChange();
}
if (data.columnsData !== "NO_DATA" && typeof(data.columnsData) === "object") {
var isValid = true;
var originalColumnIDs = [];
for (let originalColumnID in COLUMNS) {
originalColumnIDs.push(originalColumnID);
}
for (let columnSetupIndex in data.columnsData) {
var checkPresence = originalColumnIDs.indexOf(data.columnsData[columnSetupIndex].columnID);
if (checkPresence === -1) {
isValid = false;
break;
}
}
if (isValid) {
for (var columnIndex = 0; columnIndex < data.columnsData.length; columnIndex++) {
if (data.columnsData[columnIndex].data.alwaysShown !== true) {
var columnDropdownID = "entity-table-column-" + data.columnsData[columnIndex].columnID;
if (data.columnsData[columnIndex].width !== 0) {
document.getElementById(columnDropdownID).checked = false;
document.getElementById(columnDropdownID).click();
} else {
document.getElementById(columnDropdownID).checked = true;
document.getElementById(columnDropdownID).click();
}
}
}
for (columnIndex = 0; columnIndex < data.columnsData.length; columnIndex++) {
let currentColumnIndex = originalColumnIDs.indexOf(data.columnsData[columnIndex].columnID);
if (currentColumnIndex !== -1 && columnIndex !== currentColumnIndex) {
for (var i = currentColumnIndex; i > columnIndex; i--) {
swapColumns(i - 1, i);
var swappedContent = originalColumnIDs[i - 1];
originalColumnIDs[i - 1] = originalColumnIDs[i];
originalColumnIDs[i] = swappedContent;
}
}
}
} else {
EventBridge.emitWebEvent(JSON.stringify({ type: 'saveColumnsConfigSetting', columnsData: "" }));
}
}
isColumnsSettingLoaded = true;
}
});
}
refreshSortOrder();
refreshEntities();
window.addEventListener("resize", updateColumnWidths);
EventBridge.emitWebEvent(JSON.stringify({ type: 'loadConfigSetting' }));
});
augmentSpinButtons();
@ -1683,6 +1751,7 @@ function loaded() {
// close context menu when switching focus to another window
$(window).blur(function() {
entityListContextMenu.close();
closeAllEntityListMenu();
});
function closeAllEntityListMenu() {

View file

@ -668,6 +668,7 @@ SelectionManager = (function() {
var newPosition = Vec3.sum(relativePosition, targetPosition);
Entities.editEntity(id, { "position": newPosition });
}
pushCommandForSelections();
that._update(false, this);
} else {
audioFeedback.rejection();
@ -797,6 +798,7 @@ SelectionDisplay = (function() {
const COLOR_ROTATE_CURRENT_RING = { red: 255, green: 99, blue: 9 };
const COLOR_BOUNDING_EDGE = { red: 160, green: 160, blue: 160 };
const COLOR_BOUNDING_EDGE_PARENT = { red: 194, green: 123, blue: 0 };
const COLOR_BOUNDING_EDGE_PARENT_AND_CHILDREN = { red: 179, green: 0, blue: 134 };
const COLOR_BOUNDING_EDGE_CHILDREN = { red: 0, green: 168, blue: 214 };
const COLOR_SCALE_CUBE = { red: 192, green: 192, blue: 192 };
const COLOR_DEBUG_PICK_PLANE = { red: 255, green: 255, blue: 255 };
@ -1933,10 +1935,10 @@ SelectionDisplay = (function() {
var parentState = getParentState(SelectionManager.selections[0]);
if (parentState === "CHILDREN") {
handleBoundingBoxColor = COLOR_BOUNDING_EDGE_CHILDREN;
} else {
if (parentState === "PARENT" || parentState === "PARENT_CHILDREN") {
handleBoundingBoxColor = COLOR_BOUNDING_EDGE_PARENT;
}
} else if (parentState === "PARENT") {
handleBoundingBoxColor = COLOR_BOUNDING_EDGE_PARENT;
} else if (parentState === "PARENT_CHILDREN") {
handleBoundingBoxColor = COLOR_BOUNDING_EDGE_PARENT_AND_CHILDREN;
}
}

View file

@ -201,11 +201,11 @@ TabBar {
HifiControls.Button {
id: importButton
text: "Import Entities (.json)"
text: "Import Entities (.json) from a File"
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors.right: parent.right
anchors.rightMargin: 30
anchors.right: parent.horizontalCenter
anchors.rightMargin: 10
anchors.left: parent.left
anchors.leftMargin: 30
anchors.top: assetServerButton.bottom
@ -217,6 +217,25 @@ TabBar {
});
}
}
HifiControls.Button {
id: importButtonFromUrl
text: "Import Entities (.json) from a URL"
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors.right: parent.right
anchors.rightMargin: 30
anchors.left: parent.horizontalCenter
anchors.leftMargin: 10
anchors.top: assetServerButton.bottom
anchors.topMargin: 20
onClicked: {
editRoot.sendToScript({
method: "newEntityButtonClicked",
params: { buttonName: "importEntitiesFromUrlButton" }
});
}
}
}
} // Flickable
}

View file

@ -207,11 +207,11 @@ TabBar {
HifiControls.Button {
id: importButton
text: "Import Entities (.json)"
text: "Import Entities (.json) from a File"
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors.right: parent.right
anchors.rightMargin: 55
anchors.right: parent.horizontalCenter
anchors.rightMargin: 10
anchors.left: parent.left
anchors.leftMargin: 55
anchors.top: assetServerButton.bottom
@ -223,6 +223,25 @@ TabBar {
});
}
}
HifiControls.Button {
id: importButtonFromUrl
text: "Import Entities (.json) from a URL"
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors.right: parent.right
anchors.rightMargin: 55
anchors.left: parent.horizontalCenter
anchors.leftMargin: 10
anchors.top: assetServerButton.bottom
anchors.topMargin: 20
onClicked: {
editRoot.sendToScript({
method: "newEntityButtonClicked",
params: { buttonName: "importEntitiesFromUrlButton" }
});
}
}
}
} // Flickable
}

View file

@ -109,7 +109,6 @@ table {
thead {
font-family: Raleway-Regular;
font-size: 12px;
text-transform: uppercase;
background-color: #1c1c1c;
padding: 1px 0;
border-bottom: 1px solid #575757;
@ -184,6 +183,15 @@ tr.selected + tr.selected {
border-top: 1px solid #2e2e2e;
}
tr.last-selected {
color: #000000;
background-color: #0064ef;
}
tr.last-selected + tr.last-selected {
border-top: 1px solid #2e2e2e;
}
th {
text-align: center;
word-wrap: nowrap;

View file

@ -6,19 +6,18 @@ import boto3
import glob
from github import Github
def main():
bucket_name = os.environ['BUCKET_NAME']
bucket_name = os.environ['UPLOAD_BUCKET']
upload_prefix = os.environ['UPLOAD_PREFIX']
release_number = os.environ['RELEASE_NUMBER']
full_prefix = upload_prefix + '/' + release_number[0:-2] + '/' + release_number
full_prefix = upload_prefix + '/' + release_number
S3 = boto3.client('s3')
path = os.path.join(os.getcwd(), os.environ['ARTIFACT_PATTERN'])
files = glob.glob(path, recursive=False)
for archiveFile in files:
filePath, fileName = os.path.split(archiveFile)
S3.upload_file(os.path.join(filePath, fileName), bucket_name, full_prefix + '/' + fileName)
print("Uploaded Artifact to S3: https://{}.s3-us-west-2.amazonaws.com/{}/{}".format(bucket_name, full_prefix, fileName))
print("Uploaded Artifact to S3: https://{}.s3-eu-west-3.amazonaws.com/{}/{}".format(bucket_name, full_prefix, fileName))
print("Finished")
main()