diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml index 9ef4728cb0..824c279845 100644 --- a/.github/workflows/master_build.yml +++ b/.github/workflows/master_build.yml @@ -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 }} diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 065ab12abc..16931e8c26 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -470,7 +470,9 @@ void EntityScriptServer::resetEntitiesScriptEngine() { scriptEngines->runScriptInitializers(newEngine); newEngine->runInThread(); auto newEngineSP = qSharedPointerCast(newEngine); - DependencyManager::get()->setEntitiesScriptEngine(newEngineSP); + // On the entity script server, these are the same + DependencyManager::get()->setPersistentEntitiesScriptEngine(newEngineSP); + DependencyManager::get()->setNonPersistentEntitiesScriptEngine(newEngineSP); if (_entitiesScriptEngine) { disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated, diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 013eb98f84..752fb25d20 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -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 diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 18d07fa6ef..ea38a30f49 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -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 + "
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?

You can always create a new domain ID later."; + if (justConnected) { + errorText += " just save your new access token?

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(); } - }); + } }); } diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 18926fc805..7126c525bc 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -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); diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 694fd6158f..a4d05d9bc4 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -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 : ""; diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 1da20838b6..918a0a2ca6 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -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; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b76e3f8dca..c1f972eb30 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -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{ CACHEBUST_SCRIPT_REQUIRE_SETTING_NAME, false }.set(enable); +} + void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); diff --git a/interface/src/Application.h b/interface/src/Application.h index 16dadc8bf0..5cb5fdd5c0 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -472,6 +472,8 @@ public slots: void setIsInterstitialMode(bool interstitialMode); void updateVerboseLogging(); + + void setCachebustRequire(); void changeViewAsNeeded(float boomLength); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e175309f06..19cc7eacaa 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -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()->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()->clearCache(); #include "Menu.h" #include @@ -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) diff --git a/interface/src/Menu.h b/interface/src/Menu.h index d33b3b0f5e..cac8e77f9e 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -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)"; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index b5ed4b767d..2f9c8c4b3a 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -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()->runScriptInitializers(_entitiesScriptEngine); - _entitiesScriptEngine->runInThread(); - auto entitiesScriptEngineProvider = qSharedPointerCast(_entitiesScriptEngine); +void EntityTreeRenderer::setupEntityScriptEngineSignals(const ScriptEnginePointer& scriptEngine) { auto entityScriptingInterface = DependencyManager::get(); - 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()->runScriptInitializers(_persistentEntitiesScriptEngine); + _persistentEntitiesScriptEngine->runInThread(); + auto entitiesScriptEngineProvider = qSharedPointerCast(_persistentEntitiesScriptEngine); + auto entityScriptingInterface = DependencyManager::get(); + 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()->runScriptInitializers(_nonPersistentEntitiesScriptEngine); + _nonPersistentEntitiesScriptEngine->runInThread(); + auto entitiesScriptEngineProvider = qSharedPointerCast(_nonPersistentEntitiesScriptEngine); + DependencyManager::get()->setNonPersistentEntitiesScriptEngine(entitiesScriptEngineProvider); + + setupEntityScriptEngineSignals(_nonPersistentEntitiesScriptEngine); } void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { leaveDomainAndNonOwnedEntities(); // unload and stop the engine - if (_entitiesScriptEngine) { - QList entitiesWithEntityScripts = _entitiesScriptEngine->getListOfEntityScriptIDs(); + if (_nonPersistentEntitiesScriptEngine) { + QList 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 savedEntities; std::unordered_set 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(_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); } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 149b23702f..f7623aad10 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -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& entitiesContainingAvatar); @@ -196,7 +198,8 @@ private: QSet _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 _getPrevRayPickResultOperator; std::function _setPrecisionPickingOperator; - bool _mouseAndPreloadSignalHandlersConnected { false }; - class LayeredZone { public: LayeredZone(std::shared_ptr zone) : zone(zone), id(zone->getID()), volume(zone->getVolumeEstimate()) {} diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 05947551ba..36beb9f0d3 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1046,18 +1046,26 @@ QSizeF EntityScriptingInterface::textSize(const QUuid& id, const QString& text) return EntityTree::textSize(id, text); } -void EntityScriptingInterface::setEntitiesScriptEngine(QSharedPointer engine) { +void EntityScriptingInterface::setPersistentEntitiesScriptEngine(QSharedPointer engine) { std::lock_guard lock(_entitiesScriptEngineLock); - _entitiesScriptEngine = engine; + _persistentEntitiesScriptEngine = engine; +} + +void EntityScriptingInterface::setNonPersistentEntitiesScriptEngine(QSharedPointer engine) { + std::lock_guard lock(_entitiesScriptEngineLock); + _nonPersistentEntitiesScriptEngine = engine; } void EntityScriptingInterface::callEntityMethod(const QUuid& id, const QString& method, const QStringList& params) { PROFILE_RANGE(script_entities, __FUNCTION__); - - std::lock_guard lock(_entitiesScriptEngineLock); - if (_entitiesScriptEngine) { - EntityItemID entityID{ id }; - _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params); + + auto entity = getEntityTree()->findEntityByEntityItemID(id); + if (entity) { + std::lock_guard 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 lock(_entitiesScriptEngineLock); - if (_entitiesScriptEngine) { - _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params, senderNode->getUUID()); + auto entity = getEntityTree()->findEntityByEntityItemID(entityID); + if (entity) { + std::lock_guard 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()); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index fca0dad871..14d853fbaf 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -181,7 +181,8 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } - void setEntitiesScriptEngine(QSharedPointer engine); + void setPersistentEntitiesScriptEngine(QSharedPointer engine); + void setNonPersistentEntitiesScriptEngine(QSharedPointer 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)> function) { - std::lock_guard lock(_entitiesScriptEngineLock); - function(_entitiesScriptEngine); + void withEntitiesScriptEngine(std::function)> function, const EntityItemID& id) { + auto entity = getEntityTree()->findEntityByEntityItemID(id); + if (entity) { + std::lock_guard lock(_entitiesScriptEngineLock); + function((entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine); + } }; private slots: @@ -2542,7 +2546,8 @@ private: EntityTreePointer _entityTree; std::recursive_mutex _entitiesScriptEngineLock; - QSharedPointer _entitiesScriptEngine; + QSharedPointer _persistentEntitiesScriptEngine; + QSharedPointer _nonPersistentEntitiesScriptEngine; bool _bidOnSimulationOwnership { false }; diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index f9ec7797be..ebeb5936c2 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -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) { diff --git a/libraries/networking/src/ResourceRequest.cpp b/libraries/networking/src/ResourceRequest.cpp index c63bd4c563..8739cb61da 100644 --- a/libraries/networking/src/ResourceRequest.cpp +++ b/libraries/networking/src/ResourceRequest.cpp @@ -15,8 +15,14 @@ #include #include +#include #include +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()) { diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 550294d79b..042e706291 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -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(); diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index b23aa48762..0b63803a33 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -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 <<")"; diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 511d392409..4ab4458ee9 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -60,7 +60,7 @@ private: Mutex _containerLock; QMap _activeScriptRequests; - QHash _scriptCache; + QHash _scriptCache; QMultiMap _scriptUsers; }; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 88c682370c..9480b498ba 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -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 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()); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 219453875e..8cbeed58af 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -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 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1448e14c72..a72371f544 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -53,5 +53,5 @@ set(DIR "opusCodec") add_subdirectory(${DIR}) # example plugins -set(DIR "KasenAPIExample") +set(DIR "JSAPIExample") add_subdirectory(${DIR}) diff --git a/plugins/JSAPIExample/CMakeLists.txt b/plugins/JSAPIExample/CMakeLists.txt new file mode 100644 index 0000000000..a8fa0a1fd6 --- /dev/null +++ b/plugins/JSAPIExample/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME JSAPIExample) +setup_hifi_client_server_plugin(scripting) +link_hifi_libraries(shared plugins) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp new file mode 100644 index 0000000000..ed637e198b --- /dev/null +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for ::settingsFilename() +#include // for ::usecTimestampNow() +#include + +// 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(); + 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 Measure current setTimeout accuracy. + * 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 "zip" a list of keys and corresponding values to form key-value map + * 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 emulate Object.values(keyValues) + * 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 generate an integer sequence (inclusive of [from, to]) + * 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 return compressed/decompressed versions of the input data + * 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 access persistent settings stored in separate .json files + * 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 _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" diff --git a/plugins/JSAPIExample/src/plugin.json b/plugins/JSAPIExample/src/plugin.json new file mode 100644 index 0000000000..f28c7fb988 --- /dev/null +++ b/plugins/JSAPIExample/src/plugin.json @@ -0,0 +1,4 @@ +{ + "name":"JS API Example", + "version": 1 +} diff --git a/plugins/KasenAPIExample/CMakeLists.txt b/plugins/KasenAPIExample/CMakeLists.txt deleted file mode 100644 index 96ac84e10d..0000000000 --- a/plugins/KasenAPIExample/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(TARGET_NAME KasenAPIExample) -setup_hifi_client_server_plugin(scripting) -link_hifi_libraries(shared plugins avatars networking graphics gpu) diff --git a/plugins/KasenAPIExample/src/ExampleScriptPlugin.h b/plugins/KasenAPIExample/src/ExampleScriptPlugin.h deleted file mode 100644 index 76c0a494d7..0000000000 --- a/plugins/KasenAPIExample/src/ExampleScriptPlugin.h +++ /dev/null @@ -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 - -#include -#include -#include - -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()) { - qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this; - return; - } - qCWarning(logger) << "registering w/ScriptInitializerMixin..." << DependencyManager::get().data(); - DependencyManager::get()->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 \ No newline at end of file diff --git a/plugins/KasenAPIExample/src/KasenAPIExample.cpp b/plugins/KasenAPIExample/src/KasenAPIExample.cpp deleted file mode 100644 index 720c47f6cd..0000000000 --- a/plugins/KasenAPIExample/src/KasenAPIExample.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include - -#include -#include - -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 Measure current setTimeout accuracy. - * 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 Get blendshape names - * print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID))); - */ - QStringList getBlendshapeNames(const QUuid& avatarID) const { - QVector 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 Get active blendshape map - * 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& 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 getBlendshapeMap() const { - return BLENDSHAPE_LOOKUP_MAP; - } - struct States { QVector base, summed, transient; }; - States getBlendshapeStates() const { - return { - _blendshapeCoefficients, - _summedBlendshapeCoefficients, - _transientBlendshapeCoefficients - }; - } - }; - static const _HeadHelper* getAvatarHead(const QUuid& avatarID) { - auto avatars = DependencyManager::get(); - auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr; - auto head = avatar ? avatar->getHeadData() : nullptr; - return reinterpret_cast(head); - } -}; - -} - -const QLoggingCategory& example::logger{ custom_api_example::logger }; - -#include "KasenAPIExample.moc" diff --git a/plugins/KasenAPIExample/src/plugin.json b/plugins/KasenAPIExample/src/plugin.json deleted file mode 100644 index 3e6931deec..0000000000 --- a/plugins/KasenAPIExample/src/plugin.json +++ /dev/null @@ -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 - } - } -} diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 69e2e94818..099cb94988 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -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); } diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index 252481d44d..58cf4ce892 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -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); } }; diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index b22cd87a65..a5f27bd3a8 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -154,7 +154,7 @@ - +
+ +
diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index be79593511..89eac5fb2f 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -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() { diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js index ffa828affe..71edbde765 100644 --- a/scripts/system/create/entitySelectionTool/entitySelectionTool.js +++ b/scripts/system/create/entitySelectionTool/entitySelectionTool.js @@ -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; } } diff --git a/scripts/system/create/qml/EditTabView.qml b/scripts/system/create/qml/EditTabView.qml index 53f6068424..617cdd9e5a 100644 --- a/scripts/system/create/qml/EditTabView.qml +++ b/scripts/system/create/qml/EditTabView.qml @@ -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 } diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml index 0ce8d8e8d4..2403604342 100644 --- a/scripts/system/create/qml/EditToolsTabView.qml +++ b/scripts/system/create/qml/EditToolsTabView.qml @@ -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 } diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 1f1fb9c86a..4f0a833a13 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -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; diff --git a/tools/ci-scripts/upload.py b/tools/ci-scripts/upload.py index f43fbfd574..ab96e4ba24 100644 --- a/tools/ci-scripts/upload.py +++ b/tools/ci-scripts/upload.py @@ -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()