From 2645547f486c1a55ccba9fa7526fec0d26b8e1ff Mon Sep 17 00:00:00 2001 From: Kalila L Date: Thu, 27 Aug 2020 05:57:28 -0400 Subject: [PATCH 01/38] Add forceRedownload parameter to Script.require --- libraries/script-engine/src/ScriptEngine.cpp | 5 +++-- libraries/script-engine/src/ScriptEngine.h | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 3b2a122e71..641708e11b 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -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 @@ -1836,7 +1837,7 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q } // CommonJS/Node.js like require/module support -QScriptValue ScriptEngine::require(const QString& moduleId) { +QScriptValue ScriptEngine::require(const QString& moduleId, bool forceRedownload) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); @@ -1875,7 +1876,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // `delete Script.require.cache[Script.require.resolve(moduleId)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones - bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); + bool invalidateCache = (module.isUndefined() && cacheMeta.property(moduleId).isValid()) || forceRedownload; // 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 15166d572f..4e855ed125 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 @@ -421,10 +422,11 @@ public: * @function Script.require * @param {string} module - The module to use. May be a JavaScript file, a JSON file, or the name of a system module such * as "appUi" (i.e., the "appUi.js" system module JavaScript file). + * @param {bool} [forceRedownload=false] - Invalidate the cache for this module and redownload it if necessary. * @returns {object|array} The value assigned to module.exports in the JavaScript file, or the value defined * in the JSON file. */ - Q_INVOKABLE QScriptValue require(const QString& moduleId); + Q_INVOKABLE QScriptValue require(const QString& moduleId, bool forceRedownload = false); /**jsdoc * @function Script.resetModuleCache From 2a1857e9ec2a4c7627616cc1d9714a20322ec034 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Thu, 27 Aug 2020 16:39:21 -0700 Subject: [PATCH 02/38] split entity script engines into two --- .../src/scripts/EntityScriptServer.cpp | 4 +- .../src/EntityTreeRenderer.cpp | 220 +++++++++++------- .../src/EntityTreeRenderer.h | 9 +- .../entities/src/EntityScriptingInterface.cpp | 34 ++- .../entities/src/EntityScriptingInterface.h | 15 +- 5 files changed, 173 insertions(+), 109 deletions(-) diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 7c3d491470..44c388a42d 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -465,7 +465,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/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index f070c9c2f7..8e9e9d21e2 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -158,79 +158,86 @@ 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() { + _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() { + _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 +247,16 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { stopDomainAndNonOwnedEntities(); + if (_nonPersistentEntitiesScriptEngine) { + // do this here (instead of in deleter) to avoid marshalling unload signals back to this thread + _nonPersistentEntitiesScriptEngine->unloadAllEntityScripts(true); + _nonPersistentEntitiesScriptEngine->stop(); + } + + if (!_shuttingDown && _wantScripts) { + resetNonPersistentEntitiesScriptEngine(); + } + std::unordered_map savedEntities; std::unordered_set savedRenderables; // remove all entities from the scene @@ -269,11 +286,17 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { void EntityTreeRenderer::clear() { leaveAllEntities(); - // unload and stop the engine - if (_entitiesScriptEngine) { + + // unload and stop the engines + if (_nonPersistentEntitiesScriptEngine) { // do this here (instead of in deleter) to avoid marshalling unload signals back to this thread - _entitiesScriptEngine->unloadAllEntityScripts(true); - _entitiesScriptEngine->stop(); + _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(); } // reset the engine @@ -289,7 +312,8 @@ void EntityTreeRenderer::clear() { } } else { if (_wantScripts) { - resetEntitiesScriptEngine(); + resetPersistentEntitiesScriptEngine(); + resetNonPersistentEntitiesScriptEngine(); } if (scene) { for (const auto& entry : _entitiesInScene) { @@ -313,13 +337,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); + auto scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + scriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true); } } } @@ -329,7 +357,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 +370,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; @@ -655,12 +687,14 @@ 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); + auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } @@ -668,7 +702,9 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, entitiesContainingAvatar) { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); - _entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity"); + auto& entity = getTree()->findEntityByEntityItemID(entityID); + auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + scriptEngine->callEntityScriptMethod(entityID, "enterEntity"); } } _currentEntitiesInside = entitiesContainingAvatar; @@ -684,8 +720,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); @@ -703,8 +739,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(); @@ -1000,11 +1040,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(); @@ -1049,20 +1090,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(); } } @@ -1169,8 +1211,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); } } @@ -1180,8 +1223,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 1deadc254e..8538ec1c13 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -170,7 +170,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); @@ -193,7 +195,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); @@ -211,8 +214,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 fd83c99ca5..a6f9b824f0 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1045,18 +1045,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); + } } } @@ -1098,9 +1106,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()); + } } } } @@ -1331,7 +1343,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 dae0922f4a..af2ad57ea4 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -178,7 +178,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; } @@ -2497,9 +2498,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: @@ -2529,7 +2533,8 @@ private: EntityTreePointer _entityTree; std::recursive_mutex _entitiesScriptEngineLock; - QSharedPointer _entitiesScriptEngine; + QSharedPointer _persistentEntitiesScriptEngine; + QSharedPointer _nonPersistentEntitiesScriptEngine; bool _bidOnSimulationOwnership { false }; From 2e4a6ee1b530fae5e69e7033fc15f12dd2e63619 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Fri, 28 Aug 2020 11:22:08 -0700 Subject: [PATCH 03/38] fix build errors --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 8e9e9d21e2..b7d9c8cef5 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -346,7 +346,7 @@ void EntityTreeRenderer::reloadEntityScripts() { const auto& renderer = entry.second; const auto& entity = renderer->getEntity(); if (!entity->getScript().isEmpty()) { - auto scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; scriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true); } } @@ -692,7 +692,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, _currentEntitiesInside) { if (!entitiesContainingAvatar.contains(entityID)) { emit leaveEntity(entityID); - auto& entity = getTree()->findEntityByEntityItemID(entityID); + auto entity = getTree()->findEntityByEntityItemID(entityID); auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } @@ -702,7 +702,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, entitiesContainingAvatar) { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); - auto& entity = getTree()->findEntityByEntityItemID(entityID); + auto entity = getTree()->findEntityByEntityItemID(entityID); auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; scriptEngine->callEntityScriptMethod(entityID, "enterEntity"); } From 8a007a4edf5c211d7232c39652012e597f6f1011 Mon Sep 17 00:00:00 2001 From: kasenvr <52365539+kasenvr@users.noreply.github.com> Date: Mon, 7 Sep 2020 01:38:44 -0400 Subject: [PATCH 04/38] Apply suggestions from code review Co-authored-by: HifiExperiments <53453710+HifiExperiments@users.noreply.github.com> Co-authored-by: David Rowe --- libraries/script-engine/src/ScriptEngine.cpp | 2 +- libraries/script-engine/src/ScriptEngine.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 641708e11b..4f2aec0e4c 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1876,7 +1876,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId, bool forceRedownload // `delete Script.require.cache[Script.require.resolve(moduleId)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones - bool invalidateCache = (module.isUndefined() && cacheMeta.property(moduleId).isValid()) || forceRedownload; + bool invalidateCache = forceRedownload || (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 4e855ed125..4aa47834b0 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -422,7 +422,7 @@ public: * @function Script.require * @param {string} module - The module to use. May be a JavaScript file, a JSON file, or the name of a system module such * as "appUi" (i.e., the "appUi.js" system module JavaScript file). - * @param {bool} [forceRedownload=false] - Invalidate the cache for this module and redownload it if necessary. + * @param {boolean} [forceRedownload=false] - Invalidate the cache for this module and redownload it if necessary. * @returns {object|array} The value assigned to module.exports in the JavaScript file, or the value defined * in the JSON file. */ From 8eb12a873bc1217853933e88626b04bae4f9369a Mon Sep 17 00:00:00 2001 From: Kalila L Date: Thu, 17 Sep 2020 14:14:32 -0400 Subject: [PATCH 05/38] Revert forceRedownload functionality. --- libraries/script-engine/src/ScriptEngine.cpp | 5 ++--- libraries/script-engine/src/ScriptEngine.h | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 4f2aec0e4c..3b2a122e71 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -4,7 +4,6 @@ // // 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 @@ -1837,7 +1836,7 @@ QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const Q } // CommonJS/Node.js like require/module support -QScriptValue ScriptEngine::require(const QString& moduleId, bool forceRedownload) { +QScriptValue ScriptEngine::require(const QString& moduleId) { qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return unboundNullValue(); @@ -1876,7 +1875,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId, bool forceRedownload // `delete Script.require.cache[Script.require.resolve(moduleId)];` // cacheMeta is just used right now to tell deleted keys apart from undefined ones - bool invalidateCache = forceRedownload || (module.isUndefined() && cacheMeta.property(moduleId).isValid()); + bool invalidateCache = 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 4aa47834b0..15166d572f 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -4,7 +4,6 @@ // // 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 @@ -422,11 +421,10 @@ public: * @function Script.require * @param {string} module - The module to use. May be a JavaScript file, a JSON file, or the name of a system module such * as "appUi" (i.e., the "appUi.js" system module JavaScript file). - * @param {boolean} [forceRedownload=false] - Invalidate the cache for this module and redownload it if necessary. * @returns {object|array} The value assigned to module.exports in the JavaScript file, or the value defined * in the JSON file. */ - Q_INVOKABLE QScriptValue require(const QString& moduleId, bool forceRedownload = false); + Q_INVOKABLE QScriptValue require(const QString& moduleId); /**jsdoc * @function Script.resetModuleCache From 9f3978d3d5547dea5a2ec388876712e5aa6d1e49 Mon Sep 17 00:00:00 2001 From: Kalila L Date: Thu, 17 Sep 2020 18:40:01 -0400 Subject: [PATCH 06/38] Update system to use a checkbox + setting instead. --- interface/src/Application.cpp | 13 +++++++++++++ interface/src/Application.h | 2 ++ interface/src/Menu.cpp | 4 ++++ interface/src/Menu.h | 1 + libraries/script-engine/src/ScriptEngine.cpp | 5 ++++- 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fe2077f752..26ff27044d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -354,6 +354,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; @@ -1958,6 +1959,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(); @@ -2591,6 +2594,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 f42696cda0..eddf45686d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -470,6 +470,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 e460b4b56b..8870d852ba 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -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..c7ca64eb5a 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -54,6 +54,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/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 3b2a122e71..c2fa5bee04 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1873,9 +1873,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 + 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()); From 05ad300894b7b2f081934410539456e16695db8f Mon Sep 17 00:00:00 2001 From: Kalila L Date: Sun, 11 Oct 2020 00:50:41 -0400 Subject: [PATCH 07/38] CR. --- interface/src/Menu.cpp | 8 ++++---- interface/src/Menu.h | 1 + libraries/script-engine/src/ScriptEngine.cpp | 2 +- libraries/script-engine/src/ScriptEngine.h | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 1240912767..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 diff --git a/interface/src/Menu.h b/interface/src/Menu.h index c7ca64eb5a..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 diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f97b279fd0..698b495e94 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1942,7 +1942,7 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // to inspect particular entries and invalidate them by deleting the key: // `delete Script.require.cache[Script.require.resolve(moduleId)];` - // Check to see if + // 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 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 From fa3a886ce22e1259f56fc86a149c9dc65f7100ed Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 20:48:59 -0400 Subject: [PATCH 08/38] rename plugin folder --- .../CMakeLists.txt | 0 .../src/JSAPIExample.cpp} | 0 .../src/plugin.json | 0 .../KasenAPIExample/src/ExampleScriptPlugin.h | 58 ------------------- 4 files changed, 58 deletions(-) rename plugins/{KasenAPIExample => JSAPIExample}/CMakeLists.txt (100%) rename plugins/{KasenAPIExample/src/KasenAPIExample.cpp => JSAPIExample/src/JSAPIExample.cpp} (100%) rename plugins/{KasenAPIExample => JSAPIExample}/src/plugin.json (100%) delete mode 100644 plugins/KasenAPIExample/src/ExampleScriptPlugin.h diff --git a/plugins/KasenAPIExample/CMakeLists.txt b/plugins/JSAPIExample/CMakeLists.txt similarity index 100% rename from plugins/KasenAPIExample/CMakeLists.txt rename to plugins/JSAPIExample/CMakeLists.txt diff --git a/plugins/KasenAPIExample/src/KasenAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp similarity index 100% rename from plugins/KasenAPIExample/src/KasenAPIExample.cpp rename to plugins/JSAPIExample/src/JSAPIExample.cpp diff --git a/plugins/KasenAPIExample/src/plugin.json b/plugins/JSAPIExample/src/plugin.json similarity index 100% rename from plugins/KasenAPIExample/src/plugin.json rename to plugins/JSAPIExample/src/plugin.json 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 From f54b1c5fed48aa87a1ed16bd3d13affb5eb2837d Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 21:26:01 -0400 Subject: [PATCH 09/38] revamped modkit plugin example --- .../qml/hifi/dialogs/security/Security.qml | 4 +- plugins/CMakeLists.txt | 2 +- plugins/JSAPIExample/CMakeLists.txt | 4 +- plugins/JSAPIExample/src/JSAPIExample.cpp | 261 ++++++++++++------ plugins/JSAPIExample/src/plugin.json | 21 +- 5 files changed, 176 insertions(+), 116 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index b1f62633e7..cfa420955b 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/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 index 96ac84e10d..a8fa0a1fd6 100644 --- a/plugins/JSAPIExample/CMakeLists.txt +++ b/plugins/JSAPIExample/CMakeLists.txt @@ -1,3 +1,3 @@ -set(TARGET_NAME KasenAPIExample) +set(TARGET_NAME JSAPIExample) setup_hifi_client_server_plugin(scripting) -link_hifi_libraries(shared plugins avatars networking graphics gpu) +link_hifi_libraries(shared plugins) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index 720c47f6cd..1ac8d56fc5 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -1,141 +1,218 @@ // -// KasenAPIExample.cpp -// plugins/KasenAPIExample/src +// JSAPIExample.cpp +// plugins/JSAPIExample/src // -// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com -// Copyright 2019 Kasen IO -// -// Authored by: Humbletim (humbletim@gmail.com) +// 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 "ExampleScriptPlugin.h" - #include +#include +#include +#include #include #include #include #include +#include +#include -#include -#include +#include // for ::settingsFilename() +#include // for usecTimestampNow() +#include -namespace custom_api_example { +// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) +namespace REPLACE_ME_WITH_UNIQUE_NAME { -QLoggingCategory logger{ "custom_api_example" }; +static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; +static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; -class KasenAPIExample : public example::ScriptPlugin { +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 "KasenAPIExample" FILE "plugin.json") + Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json") + Q_PROPERTY(QString version MEMBER _version CONSTANT) public: - KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") { - qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); + 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: + // returns a pretty-printed representation for logging eg: print(JSAPIExample) + inline 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 = KasenAPIExample.now(); + * var start = JSAPIExample.now(); * Script.setTimeout(function () { - * var elapsed = (KasenAPIExample.now() - start)/1000; + * var elapsed = (JSAPIExample.now() - start)/1000; * print("expected (ms):", expected, "actual (ms):", elapsed); * }, expected); */ - QVariant now() const { - return usecTimestampNow(); - } + 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))); + * 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 } */ - 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 { + QVariant zip(const QStringList& keys, const QVariantList& values) const { QVariantMap out; - if (auto head = getAvatarHead(avatarID)) { - for (const auto& kv : head->getBlendshapeMap().toStdMap()) { - out[kv.first] = kv.second; - } + 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 { return keyValues.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; } - 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) }, - }; + /**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& data, int compress_level = -1) const { + return qCompress(data.toUtf8(), compress_level); + } + QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); } + + /** + * 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.value("timestamp")); + * settings.setValue("timestamp", Date.now()); + * print("(after) example::timestamp", settings.value("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 result; + return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); } 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; + const QString _version{ JSAPI_SEMANTIC_VERSION }; +}; + +// Example of how to create a QObject class that can have multiple instances created from the JS side +// JSSettingsHelper emulates a subset of QSetting APIs: +// fileName() -- full path to the scoped settings .json file +// allKeys() -- all previously stored keys available in the scoped settings file +// value(key, defaultValue) -- retrieve a stored value +// setValue(key, newValue) -- set/update a stored value +class JSSettingsHelper : public QObject { + Q_OBJECT + QString _scope; + QString _fileName; + QSharedPointer _settings; + +public: + operator bool() const { return (bool)_settings; } + JSSettingsHelper(const QString& scope, QObject* parent = nullptr) : + QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), + _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {} + ~JSSettingsHelper() { qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; } +public slots: + QString fileName() const { return _settings ? _settings->fileName() : ""; } + QString toString() const { return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); } + QVariant value(const QString& key, const QVariant& defaultValue = QVariant()) { + return _settings ? _settings->value(key, defaultValue) : defaultValue; } - struct _HeadHelper : public HeadData { - QMap getBlendshapeMap() const { - return BLENDSHAPE_LOOKUP_MAP; + bool setValue(const QString& key, const QVariant& value) { + if (_settings) { + _settings->setValue(key, value); + return true; } - 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); + return false; + } + QStringList allKeys() const { return _settings ? _settings->allKeys() : QStringList{}; } + +protected: + QString 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); } }; +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); } -const QLoggingCategory& example::logger{ custom_api_example::logger }; +} // namespace REPLACE_ME_WITH_UNIQUE_NAME -#include "KasenAPIExample.moc" +#include "JSAPIExample.moc" diff --git a/plugins/JSAPIExample/src/plugin.json b/plugins/JSAPIExample/src/plugin.json index 3e6931deec..f28c7fb988 100644 --- a/plugins/JSAPIExample/src/plugin.json +++ b/plugins/JSAPIExample/src/plugin.json @@ -1,21 +1,4 @@ { - "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 - } - } + "name":"JS API Example", + "version": 1 } From 7fb0173ef7df5b6f00637104742052fc9a1e8f64 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 21:58:32 -0400 Subject: [PATCH 10/38] clarifications per peer review --- plugins/JSAPIExample/src/JSAPIExample.cpp | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index 1ac8d56fc5..f3947b2f4f 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -73,8 +73,9 @@ public: // (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine) public slots: - // returns a pretty-printed representation for logging eg: print(JSAPIExample) - inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } + // 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 @@ -100,12 +101,16 @@ public slots: } 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 { return keyValues.values(); } + QVariant values(const QVariantMap& keyValues) const { + QVariantList values = keyValues.values(); + return values; + } /**jsdoc * Another example of returning JS Array data @@ -129,10 +134,14 @@ public slots: * 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& data, int compress_level = -1) const { - return qCompress(data.toUtf8(), compress_level); + 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; } - QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); } /** * Example of exposing a custom "managed" C++ QObject to JS @@ -149,12 +158,13 @@ public slots: */ QScriptValue getScopedSettings(const QString& scope) { auto engine = QScriptable::engine(); - if (!engine) return QScriptValue::NullValue; + 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; + raiseScriptingError(context(), "error creating scoped settings instance: " + error); + return QScriptValue::NullValue; } return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); } @@ -163,7 +173,6 @@ private: const QString _version{ JSAPI_SEMANTIC_VERSION }; }; -// Example of how to create a QObject class that can have multiple instances created from the JS side // JSSettingsHelper emulates a subset of QSetting APIs: // fileName() -- full path to the scoped settings .json file // allKeys() -- all previously stored keys available in the scoped settings file @@ -204,6 +213,7 @@ protected: } }; +// 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)) { From dedf6a6975957097bb56d344f8d5fa74f3662ee6 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 22:24:29 -0400 Subject: [PATCH 11/38] further cleanup per peer review (thanks fluffy!) --- plugins/JSAPIExample/src/JSAPIExample.cpp | 94 ++++++++++++++--------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index f3947b2f4f..d0e5a27869 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -22,7 +22,7 @@ #include #include // for ::settingsFilename() -#include // for usecTimestampNow() +#include // for ::usecTimestampNow() #include // NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) @@ -150,9 +150,9 @@ public slots: * @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.value("timestamp")); + * print("(before) example::timestamp", settings.getValue("timestamp")); * settings.setValue("timestamp", Date.now()); - * print("(after) example::timestamp", settings.value("timestamp")); + * 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 */ @@ -173,44 +173,24 @@ private: const QString _version{ JSAPI_SEMANTIC_VERSION }; }; -// JSSettingsHelper emulates a subset of QSetting APIs: -// fileName() -- full path to the scoped settings .json file -// allKeys() -- all previously stored keys available in the scoped settings file -// value(key, defaultValue) -- retrieve a stored value -// setValue(key, newValue) -- set/update a stored value +// 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; - -public: - operator bool() const { return (bool)_settings; } - JSSettingsHelper(const QString& scope, QObject* parent = nullptr) : - QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), - _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {} - ~JSSettingsHelper() { qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; } -public slots: - QString fileName() const { return _settings ? _settings->fileName() : ""; } - QString toString() const { return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); } - QVariant value(const QString& key, const QVariant& defaultValue = QVariant()) { - return _settings ? _settings->value(key, defaultValue) : defaultValue; - } - bool setValue(const QString& key, const QVariant& value) { - if (_settings) { - _settings->setValue(key, value); - return true; - } - return false; - } - QStringList allKeys() const { return _settings ? _settings->allKeys() : QStringList{}; } - -protected: - QString 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); - } + QString getLocalSettingsPath(const QString& scope) const; }; // verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance @@ -223,6 +203,48 @@ QObject* createScopedSettings(const QString& scope, QObject* parent, QString& er 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" From 5c2a8bd45938bce257af5ed15027d8b11943da35 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 31 Oct 2020 14:11:05 -0400 Subject: [PATCH 12/38] changes per CR --- plugins/JSAPIExample/src/JSAPIExample.cpp | 423 +++++++++++----------- 1 file changed, 212 insertions(+), 211 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index d0e5a27869..ed637e198b 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -28,222 +28,223 @@ // 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"; + static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; + static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; -QLoggingCategory logger{ "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); + 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 { - _settings->remove(key); + // otherwise just log the error + qCWarning(logger) << "error:" << message; } - return true; + return returnValue; } - 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 ----- + + 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 From e28b2025a97747657e8ad11657e706feb395c4d2 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 3 Nov 2020 20:47:48 +1300 Subject: [PATCH 13/38] Prompt for username or e-mail for domain login --- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 : ""; From c6728be4e8c2c52ba62b4b0e54653df67e6549ec Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 4 Nov 2020 11:23:47 +1300 Subject: [PATCH 14/38] Fix enterEntity event not firing in entithy script after content reload --- .../entities-renderer/src/EntityTreeRenderer.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index b5ed4b767d..3538f07d32 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -167,6 +167,13 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->setEntitiesScriptEngine(entitiesScriptEngineProvider); + connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) { + EntityItemPointer entity = getTree()->findEntityByID(entityID); + if (entity) { + entity->setScriptHasFinishedPreload(true); + } + }); + // Connect mouse events to entity script callbacks if (!_mouseAndPreloadSignalHandlersConnected) { @@ -205,13 +212,6 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { _entitiesScriptEngine->callEntityScriptMethod(entityID, "hoverLeaveEntity", event); }); - connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) { - EntityItemPointer entity = getTree()->findEntityByID(entityID); - if (entity) { - entity->setScriptHasFinishedPreload(true); - } - }); - _mouseAndPreloadSignalHandlersConnected = true; } } From b7933788301995cc58579988e5939f73193fc618 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:12:04 -0500 Subject: [PATCH 15/38] Undo-redo for "Move Selected Entities to Avatar" This adds the undo/redo to the action "Move Selected Entities to Avatar". The action is now recorded in the undo history. --- scripts/system/create/entitySelectionTool/entitySelectionTool.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js index ffa828affe..97e36d818e 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(); From 1cf0e2c00a6c75f3a3fa263932e24bdb7922a29f Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:23:29 -0500 Subject: [PATCH 16/38] "Teleport to Selected Entities" to Actions menu This moves "Teleport to Selected Entities" from "Selection" to "Actions" menu. --- .../create/entityList/html/entityList.html | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index b22cd87a65..b3a354fdda 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -154,7 +154,13 @@ - + +
- - - + From dcb1493eaf4e265b552fd1dc21db5b5011230f9e Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Thu, 5 Nov 2020 21:49:22 -0500 Subject: [PATCH 17/38] Rename "Select Family" This renames: "Select Family" for "Select Parent And All Its Children" "Select Top Family" for "Select Top Parent And All Its Children" --- scripts/system/create/entityList/html/entityList.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index b3a354fdda..87f7f535d6 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -215,13 +215,13 @@ From 26405300fefee76bd7de8aebed8dee7453731fc6 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 8 Nov 2020 21:35:17 +1300 Subject: [PATCH 18/38] Handle username or email for domain login --- domain-server/src/DomainGatekeeper.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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); From ac46e9a4929f77e7837a0ef2ceab83f06474116d Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:28:54 -0500 Subject: [PATCH 19/38] Remove uppercase from Create App List header Remove uppercase from Create App List header The columns headers were forced to be displayed in uppercase This was causing the wrong icon to be displayed for some columns like "script". Uppercase wasn't necessary, the header was looking overloaded when many columns are displayed. --- scripts/system/html/css/edit-style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 1f1fb9c86a..14cfe5e431 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; From c573242043ab3ca921d2d308d7d0fef71cd06c59 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:31:48 -0500 Subject: [PATCH 20/38] Add Setting name to persist the columns setup Add a Setting name to persist the columns' visibility and ordering. --- scripts/system/create/edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 69e2e94818..cd987140d8 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -160,6 +160,7 @@ 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_EDIT_PREFIX = "Edit/"; From f3dea2fbad55082cb1d0176dfde177f7e0e4036f Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:34:07 -0500 Subject: [PATCH 21/38] Add the persistence of the columns setup Add the persistence of the columns' visibility and ordering. --- scripts/system/create/entityList/entityList.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index 252481d44d..d8930ebc2d 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -371,6 +371,14 @@ EntityListTool = function(shouldUseEditTabletApp) { SelectionManager.teleportToEntity(); } else if (data.type === 'moveEntitySelectionToAvatar') { SelectionManager.moveEntitiesSelectionToAvatar(); + } else if (data.type === 'loadColumnsConfigSetting') { + var columnsData = Settings.getValue(SETTING_EDIT_PREFIX + SETTING_EDITOR_COLUMNS_SETUP, "NO_DATA"); + emitJSONScriptEvent({ + "type": "loadedColumnsSetup", + "columnsData": columnsData + }); + } else if (data.type === 'saveColumnsConfigSetting') { + Settings.setValue(SETTING_EDIT_PREFIX + SETTING_EDITOR_COLUMNS_SETUP, data.columnsData); } }; From 233d68952a928f9395a4d02ac907df6a8ea84ca4 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:35:13 -0500 Subject: [PATCH 22/38] Add the persistence of the columns setup Add the persistence of the columns' visibility and ordering. --- .../create/entityList/html/js/entityList.js | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index be79593511..3ff8328132 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 **"; @@ -283,6 +283,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 +334,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 +621,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); @@ -1409,6 +1412,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 +1667,59 @@ function loaded() { } else { document.getElementById("hmdmultiselect").style.display = "none"; } + } else if (data.type === "loadedColumnsSetup") { + 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: 'loadColumnsConfigSetting' })); }); augmentSpinButtons(); From 44db0bb866b724329e853c686160d2772a274dcf Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 22:02:42 -0500 Subject: [PATCH 23/38] Add selection color Parent & Child entities Add in-world selection color for entities that are Parent & Child at the same time. When 1 entity is selected (in-world): if the entity is a Top Parent, the selection color of the bounding box is Orange. if the entity is Parent and Child, the selection color of the bounding box is Majenta. if the entity is only a Child, then the selection color of the bounding box is Cyan. If not involved in any parent line, or if the selection is multiple, then the selection color of the bounding box is Light Grey. --- .../create/entitySelectionTool/entitySelectionTool.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js index 97e36d818e..71edbde765 100644 --- a/scripts/system/create/entitySelectionTool/entitySelectionTool.js +++ b/scripts/system/create/entitySelectionTool/entitySelectionTool.js @@ -798,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 }; @@ -1934,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; } } From fc40a401d70416f6cc4329a9daeca506c2d3ddcb Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 22:37:33 -0500 Subject: [PATCH 24/38] Selection and Actions menu close with the window Bug fixed: The "Selection" and "Actions" menu now close when we close the Entity list window or the Create App. --- scripts/system/create/entityList/html/js/entityList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index 3ff8328132..aa23784eea 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -1735,6 +1735,7 @@ function loaded() { // close context menu when switching focus to another window $(window).blur(function() { entityListContextMenu.close(); + closeAllEntityListMenu(); }); function closeAllEntityListMenu() { From 9c48acf1d332832234b897a996e05616351c4b9c Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Mon, 9 Nov 2020 23:54:39 -0500 Subject: [PATCH 25/38] Add style for last-selected Add style for last-selected --- scripts/system/html/css/edit-style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 14cfe5e431..4f0a833a13 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -183,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; From fb516cf13c0f36cbab2d27ad49e1f6a9403f957f Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Tue, 10 Nov 2020 00:01:15 -0500 Subject: [PATCH 26/38] Highlight the last selected entity in the List The last selected entity is now displayed in darker blue in the list (if present per radius search) This makes clear which entity could become the parent. (This enlights some weird behavior with the Selection, making this hazardous to figure which one was the true last selected entity. To be addressed later, it's already better now.) --- .../create/entityList/html/js/entityList.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index aa23784eea..c0b28532cb 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -188,6 +188,8 @@ let selectedEntities = []; let entityList = null; // The ListView let hmdMultiSelectMode = false; + +let lastSelectedEntity; /** * @type EntityListContextMenu */ @@ -1047,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]; @@ -1066,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; @@ -1135,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 = ''; } From 1167ce7a1c9094823ad4045f80da9cdf4bd0b417 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Tue, 10 Nov 2020 21:56:44 -0500 Subject: [PATCH 27/38] "Teleport To Selected Entities" back to Selection This returns "Teleport To Selected Entities" back to the "Selection..." menu. After having tried a certain time what was suggested (having it under "Actions..." menu) This is clearly easy to confuse it with "Move Selected Entities to Avatar" "Teleport To Selected Entities" is about position your-self to see what you have "Selected" (which make this a Selection Tools or an assistant to it) while "Move Selected Entities to Avatar" is clearly an action. as it moves the entities to a new position. --- .../system/create/entityList/html/entityList.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index 87f7f535d6..63e774c787 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -155,12 +155,6 @@ -
+ + From 38e15b62085f8528fd4012aeba4127585379a335 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Tue, 10 Nov 2020 22:51:32 -0500 Subject: [PATCH 28/38] Bug fix: CTRL-Click was inverting the selection. Bug fix: EntityList, CTRL-Click was adding the last selected entity as the first selection the selection stack. The first selected item was systematically the last item of the selection. This was causing unpredictable results with "Parent Entities To The Last Selected". (This bug becomes evident by highlighting the "Last Selected" in the entity list. --- scripts/system/create/entityList/html/js/entityList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index c0b28532cb..bb84f35d6d 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -770,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; From 3b2c219e53c6ea6e753a44260bdc54a731bd95d2 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Tue, 10 Nov 2020 21:39:15 -0800 Subject: [PATCH 29/38] possibly more robust fix? --- .../src/EntityTreeRenderer.cpp | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 77c85f0255..2f9c8c4b3a 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -205,6 +205,12 @@ void EntityTreeRenderer::setupEntityScriptEngineSignals(const ScriptEnginePointe } 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); @@ -217,6 +223,12 @@ void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() { } 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); @@ -247,12 +259,6 @@ void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { stopDomainAndNonOwnedEntities(); - if (_nonPersistentEntitiesScriptEngine) { - // do this here (instead of in deleter) to avoid marshalling unload signals back to this thread - _nonPersistentEntitiesScriptEngine->unloadAllEntityScripts(true); - _nonPersistentEntitiesScriptEngine->stop(); - } - if (!_shuttingDown && _wantScripts) { resetNonPersistentEntitiesScriptEngine(); } @@ -287,21 +293,21 @@ void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { void EntityTreeRenderer::clear() { leaveAllEntities(); - // 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(); - } - // 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) { @@ -345,7 +351,7 @@ void EntityTreeRenderer::reloadEntityScripts() { for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const auto& entity = renderer->getEntity(); - if (!entity->getScript().isEmpty()) { + if (entity && !entity->getScript().isEmpty()) { auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; scriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true); } @@ -696,8 +702,10 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { if (!entitiesContainingAvatar.contains(entityID)) { emit leaveEntity(entityID); auto entity = getTree()->findEntityByEntityItemID(entityID); - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + if (entity) { + auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + } } } @@ -706,8 +714,10 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); auto entity = getTree()->findEntityByEntityItemID(entityID); - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - scriptEngine->callEntityScriptMethod(entityID, "enterEntity"); + if (entity) { + auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; + scriptEngine->callEntityScriptMethod(entityID, "enterEntity"); + } } } _currentEntitiesInside = entitiesContainingAvatar; From 428db065b548ddff807467b574708702840f87a8 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Wed, 11 Nov 2020 23:41:31 -0500 Subject: [PATCH 30/38] User Preference: Entity List Default Radius Add a new User Preference for the Default value of the Entity List Radius. This is available in the Edit menu. If changed, this value will be used as default Radius for the next time the script will be loaded. --- scripts/system/create/entityList/html/js/entityList.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index bb84f35d6d..c8e09e2b37 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -1679,7 +1679,11 @@ function loaded() { } else { document.getElementById("hmdmultiselect").style.display = "none"; } - } else if (data.type === "loadedColumnsSetup") { + } 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 = []; @@ -1731,7 +1735,7 @@ function loaded() { window.addEventListener("resize", updateColumnWidths); - EventBridge.emitWebEvent(JSON.stringify({ type: 'loadColumnsConfigSetting' })); + EventBridge.emitWebEvent(JSON.stringify({ type: 'loadConfigSetting' })); }); augmentSpinButtons(); From f22c7be72e9dba95cb452af5c984b613b1241417 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Wed, 11 Nov 2020 23:42:29 -0500 Subject: [PATCH 31/38] User Preference: Entity List Default Radius Add a new User Preference for the Default value of the Entity List Radius. This is available in the Edit menu. If changed, this value will be used as default Radius for the next time the script will be loaded. --- scripts/system/create/entityList/entityList.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index d8930ebc2d..58cf4ce892 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -371,14 +371,16 @@ EntityListTool = function(shouldUseEditTabletApp) { SelectionManager.teleportToEntity(); } else if (data.type === 'moveEntitySelectionToAvatar') { SelectionManager.moveEntitiesSelectionToAvatar(); - } else if (data.type === 'loadColumnsConfigSetting') { - var columnsData = Settings.getValue(SETTING_EDIT_PREFIX + SETTING_EDITOR_COLUMNS_SETUP, "NO_DATA"); + } 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": "loadedColumnsSetup", - "columnsData": columnsData + "type": "loadedConfigSetting", + "columnsData": columnsData, + "defaultRadius": defaultRadius }); } else if (data.type === 'saveColumnsConfigSetting') { - Settings.setValue(SETTING_EDIT_PREFIX + SETTING_EDITOR_COLUMNS_SETUP, data.columnsData); + Settings.setValue(SETTING_EDITOR_COLUMNS_SETUP, data.columnsData); } }; From 7bec7cbaa1adceb28a7173fd5429f6a6d99af80e Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Wed, 11 Nov 2020 23:43:49 -0500 Subject: [PATCH 32/38] User Preference: Entity List Default Radius Add a new User Preference for the Default value of the Entity List Radius. This is available in the Edit menu. If changed, this value will be used as default Radius for the next time the script will be loaded. --- scripts/system/create/edit.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index cd987140d8..a457b62fe6 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -155,12 +155,14 @@ var MENU_CREATE_ENTITIES_GRABBABLE = "Create Entities As Grabbable (except Zones 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/"; @@ -1509,6 +1511,11 @@ function setupModelMenus() { isCheckable: true, isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false" }); + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: MENU_ENTITY_LIST_DEFAULT_RADIUS, + afterItem: MENU_SHOW_ZONES_IN_EDIT_MODE + }); Entities.setLightsArePickable(false); } @@ -1542,6 +1549,7 @@ function cleanupModelMenus() { 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("Edit", MENU_ENTITY_LIST_DEFAULT_RADIUS); } Script.scriptEnding.connect(function () { @@ -1882,6 +1890,17 @@ 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"); @@ -1912,7 +1931,7 @@ function handleMenuEvent(menuItem) { Window.browseAsync("Select Model to Import", "", "*.json"); } else { Window.promptTextChanged.connect(onPromptTextChanged); - Window.promptAsync("URL of SVO to import", ""); + Window.promptAsync("URL of a .json to import", ""); } } else if (menuItem === "Select All Entities In Box") { selectAllEntitiesInCurrentSelectionBox(false); @@ -1924,6 +1943,9 @@ function handleMenuEvent(menuItem) { 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); } From 636d52b306ab5e6de49eb5eca6d700ce997a9bdb Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:48:15 -0500 Subject: [PATCH 33/38] =?UTF-8?q?Add=20=E2=80=9CImport=20Entities=20(.json?= =?UTF-8?q?)=20from=20a=20URL=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The action “Import Entities (.json) from a URL” has been added to the “Create” Tab. (It was available only in the Edit menu.) --- scripts/system/create/qml/EditTabView.qml | 25 ++++++++++++++++--- .../system/create/qml/EditToolsTabView.qml | 25 ++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) 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..8379b47259 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 } From e0cac2b95cac384b1c91e9e7cbbf1fd664412e96 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:52:24 -0500 Subject: [PATCH 34/38] Clean up of the Edit Menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Create App. Menu Items under the “Edit” menu have been cleaned up. All the actions being transferred has been removed. (some was not even decently usable from there) A new sub-menu “Create Application – Preferences” has been implemented to regroup the remaining menu items that are all User Preferences. Eventually in the future, these might be moved to the Create App. UI. For now, it will be OK, it's already cleaner. --- scripts/system/create/edit.js | 160 ++++++++-------------------------- 1 file changed, 35 insertions(+), 125 deletions(-) diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index a457b62fe6..099cb94988 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -146,11 +146,11 @@ 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"; @@ -270,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)) { @@ -881,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() { @@ -1381,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", @@ -1399,120 +1400,66 @@ 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: "Edit", + menuName: SUBMENU_ENTITY_EDITOR_PREFERENCES, menuItemName: MENU_ENTITY_LIST_DEFAULT_RADIUS, afterItem: MENU_SHOW_ZONES_IN_EDIT_MODE }); @@ -1526,30 +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("Edit", MENU_ENTITY_LIST_DEFAULT_RADIUS); + 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 () { @@ -1902,41 +1835,18 @@ function onPromptTextChangedDefaultRadiusUserPref(prompt) { } 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 a .json 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) { From 33eb01a6c7bedb1da6b3041125dcdebe93db4d3a Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Sat, 14 Nov 2020 22:33:38 -0500 Subject: [PATCH 35/38] Minor code adjustments Minor code adjustments --- scripts/system/create/entityList/html/entityList.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index 63e774c787..a5f27bd3a8 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -225,7 +225,7 @@ - + From 2475deac930db55752f5ac7188dbe6be29c0b9e5 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Sat, 14 Nov 2020 22:36:28 -0500 Subject: [PATCH 36/38] Minor code adjustments Minor code adjustments --- scripts/system/create/entityList/html/js/entityList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index c8e09e2b37..89eac5fb2f 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -1714,9 +1714,9 @@ function loaded() { 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]; + swapColumns(i - 1, i); + var swappedContent = originalColumnIDs[i - 1]; + originalColumnIDs[i - 1] = originalColumnIDs[i]; originalColumnIDs[i] = swappedContent; } } From da2737de530d5a91550116d38ea3215f01cd8d15 Mon Sep 17 00:00:00 2001 From: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Date: Sat, 14 Nov 2020 22:39:47 -0500 Subject: [PATCH 37/38] Minor Code Adjustements Minor Code Adjustements --- scripts/system/create/qml/EditToolsTabView.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml index 8379b47259..2403604342 100644 --- a/scripts/system/create/qml/EditToolsTabView.qml +++ b/scripts/system/create/qml/EditToolsTabView.qml @@ -211,7 +211,7 @@ TabBar { color: hifi.buttons.black colorScheme: hifi.colorSchemes.dark anchors.right: parent.horizontalCenter - anchors.rightMargin: 10 + anchors.rightMargin: 10 anchors.left: parent.left anchors.leftMargin: 55 anchors.top: assetServerButton.bottom @@ -241,7 +241,7 @@ TabBar { params: { buttonName: "importEntitiesFromUrlButton" } }); } - } + } } } // Flickable } From 21f8eac9362ad36e151ab8b24f1b5f59807ebddc Mon Sep 17 00:00:00 2001 From: kasenvr <52365539+kasenvr@users.noreply.github.com> Date: Mon, 16 Nov 2020 22:35:46 -0500 Subject: [PATCH 38/38] Revert "Split Local and owned Avatar Entity scripts into their own ScriptEngine" --- .../src/scripts/EntityScriptServer.cpp | 4 +- .../src/EntityTreeRenderer.cpp | 230 +++++++----------- .../src/EntityTreeRenderer.h | 9 +- .../entities/src/EntityScriptingInterface.cpp | 34 +-- .../entities/src/EntityScriptingInterface.h | 15 +- 5 files changed, 109 insertions(+), 183 deletions(-) diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 16931e8c26..065ab12abc 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -470,9 +470,7 @@ void EntityScriptServer::resetEntitiesScriptEngine() { scriptEngines->runScriptInitializers(newEngine); newEngine->runInThread(); auto newEngineSP = qSharedPointerCast(newEngine); - // On the entity script server, these are the same - DependencyManager::get()->setPersistentEntitiesScriptEngine(newEngineSP); - DependencyManager::get()->setNonPersistentEntitiesScriptEngine(newEngineSP); + DependencyManager::get()->setEntitiesScriptEngine(newEngineSP); if (_entitiesScriptEngine) { disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 2f9c8c4b3a..3538f07d32 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -158,98 +158,79 @@ render::ItemID EntityTreeRenderer::renderableIdForEntityId(const EntityItemID& i int EntityTreeRenderer::_entitiesScriptEngineCount = 0; -void EntityTreeRenderer::setupEntityScriptEngineSignals(const ScriptEnginePointer& scriptEngine) { +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); auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->setEntitiesScriptEngine(entitiesScriptEngineProvider); - 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, 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, 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(scriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) { + connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptPreloadFinished, [&](const EntityItemID& entityID) { EntityItemPointer entity = getTree()->findEntityByID(entityID); if (entity) { entity->setScriptHasFinishedPreload(true); } }); -} -void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() { - if (_persistentEntitiesScriptEngine) { - _persistentEntitiesScriptEngine->unloadAllEntityScripts(true); - _persistentEntitiesScriptEngine->stop(); - _persistentEntitiesScriptEngine->waitTillDoneRunning(); - _persistentEntitiesScriptEngine->disconnectNonEssentialSignals(); + // 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::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::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); + }); + + _mouseAndPreloadSignalHandlersConnected = true; } - _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 (_nonPersistentEntitiesScriptEngine) { - QList entitiesWithEntityScripts = _nonPersistentEntitiesScriptEngine->getListOfEntityScriptIDs(); + if (_entitiesScriptEngine) { + QList entitiesWithEntityScripts = _entitiesScriptEngine->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())) { - _nonPersistentEntitiesScriptEngine->unloadEntityScript(entityID, true); + if (_currentEntitiesInside.contains(entityID)) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + } + _entitiesScriptEngine->unloadEntityScript(entityID, true); } } } @@ -259,10 +240,6 @@ 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 @@ -292,22 +269,16 @@ 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) { @@ -318,8 +289,7 @@ void EntityTreeRenderer::clear() { } } else { if (_wantScripts) { - resetPersistentEntitiesScriptEngine(); - resetNonPersistentEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } if (scene) { for (const auto& entry : _entitiesInScene) { @@ -343,17 +313,13 @@ void EntityTreeRenderer::clear() { } void EntityTreeRenderer::reloadEntityScripts() { - _persistentEntitiesScriptEngine->unloadAllEntityScripts(); - _persistentEntitiesScriptEngine->resetModuleCache(); - _nonPersistentEntitiesScriptEngine->unloadAllEntityScripts(); - _nonPersistentEntitiesScriptEngine->resetModuleCache(); - + _entitiesScriptEngine->unloadAllEntityScripts(); + _entitiesScriptEngine->resetModuleCache(); for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const auto& entity = renderer->getEntity(); - if (entity && !entity->getScript().isEmpty()) { - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - scriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true); + if (!entity->getScript().isEmpty()) { + _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), resolveScriptURL(entity->getScript()), true); } } } @@ -363,8 +329,7 @@ void EntityTreeRenderer::init() { EntityTreePointer entityTree = std::static_pointer_cast(_tree); if (_wantScripts) { - resetPersistentEntitiesScriptEngine(); - resetNonPersistentEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } forceRecheckEntities(); // setup our state to force checking our inside/outsideness of entities @@ -376,11 +341,8 @@ void EntityTreeRenderer::init() { } void EntityTreeRenderer::shutdown() { - 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 + if (_entitiesScriptEngine) { + _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential } _shuttingDown = true; @@ -696,16 +658,12 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { // EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts // for entity IDs that no longer exist. - if (_persistentEntitiesScriptEngine && _nonPersistentEntitiesScriptEngine) { + if (_entitiesScriptEngine) { // 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); - auto entity = getTree()->findEntityByEntityItemID(entityID); - if (entity) { - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); - } + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } @@ -713,11 +671,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { foreach(const EntityItemID& entityID, entitiesContainingAvatar) { if (!_currentEntitiesInside.contains(entityID)) { emit enterEntity(entityID); - auto entity = getTree()->findEntityByEntityItemID(entityID); - if (entity) { - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - scriptEngine->callEntityScriptMethod(entityID, "enterEntity"); - } + _entitiesScriptEngine->callEntityScriptMethod(entityID, "enterEntity"); } } _currentEntitiesInside = entitiesContainingAvatar; @@ -733,8 +687,8 @@ void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() { EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); if (entityItem && !(entityItem->isLocalEntity() || entityItem->isMyAvatarEntity())) { emit leaveEntity(entityID); - if (_nonPersistentEntitiesScriptEngine) { - _nonPersistentEntitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } else { currentEntitiesInsideToSave.insert(entityID); @@ -752,12 +706,8 @@ 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); - EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); - if (entityItem) { - auto& scriptEngine = (entityItem->isLocalEntity() || entityItem->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - if (scriptEngine) { - scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); - } + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } _currentEntitiesInside.clear(); @@ -1053,12 +1003,11 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { return; } - auto& scriptEngine = (itr->second->getEntity()->isLocalEntity() || itr->second->getEntity()->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - if (_tree && !_shuttingDown && scriptEngine && !itr->second->getEntity()->getScript().isEmpty()) { + if (_tree && !_shuttingDown && _entitiesScriptEngine && !itr->second->getEntity()->getScript().isEmpty()) { if (_currentEntitiesInside.contains(entityID)) { - scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } - scriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } auto scene = _viewState->getMain3DScene(); @@ -1103,21 +1052,20 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool if (!entity) { return; } - auto& scriptEngine = (entity->isLocalEntity() || entity->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - bool shouldLoad = entity->shouldPreloadScript() && scriptEngine; + bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) { - if (scriptEngine) { + if (_entitiesScriptEngine) { if (_currentEntitiesInside.contains(entityID)) { - scriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } - scriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID); } entity->scriptHasUnloaded(); } if (shouldLoad) { entity->setScriptHasFinishedPreload(false); - scriptEngine->loadEntityScript(entityID, resolveScriptURL(scriptUrl), reload); + _entitiesScriptEngine->loadEntityScript(entityID, resolveScriptURL(scriptUrl), reload); entity->scriptHasPreloaded(); } } @@ -1224,9 +1172,8 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons if ((myNodeID == entityASimulatorID && entityAIsDynamic) || (myNodeID == entityBSimulatorID && (!entityAIsDynamic || entityASimulatorID.isNull()))) { playEntityCollisionSound(entityA, collision); emit collisionWithEntity(idA, idB, collision); - auto& scriptEngine = (entityA->isLocalEntity() || entityA->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - if (scriptEngine) { - scriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); } } @@ -1236,9 +1183,8 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons Collision invertedCollision(collision); invertedCollision.invert(); emit collisionWithEntity(idB, idA, invertedCollision); - auto& scriptEngine = (entityB->isLocalEntity() || entityB->isMyAvatarEntity()) ? _persistentEntitiesScriptEngine : _nonPersistentEntitiesScriptEngine; - if (scriptEngine) { - scriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index f7623aad10..149b23702f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -173,9 +173,7 @@ private: EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); } render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); } - void resetPersistentEntitiesScriptEngine(); - void resetNonPersistentEntitiesScriptEngine(); - void setupEntityScriptEngineSignals(const ScriptEnginePointer& scriptEngine); + void resetEntitiesScriptEngine(); void findBestZoneAndMaybeContainingEntities(QSet& entitiesContainingAvatar); @@ -198,8 +196,7 @@ private: QSet _currentEntitiesInside; bool _wantScripts; - 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 + ScriptEnginePointer _entitiesScriptEngine; void playEntityCollisionSound(const EntityItemPointer& entity, const Collision& collision); @@ -217,6 +214,8 @@ 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 36beb9f0d3..05947551ba 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1046,26 +1046,18 @@ QSizeF EntityScriptingInterface::textSize(const QUuid& id, const QString& text) return EntityTree::textSize(id, text); } -void EntityScriptingInterface::setPersistentEntitiesScriptEngine(QSharedPointer engine) { +void EntityScriptingInterface::setEntitiesScriptEngine(QSharedPointer engine) { std::lock_guard lock(_entitiesScriptEngineLock); - _persistentEntitiesScriptEngine = engine; -} - -void EntityScriptingInterface::setNonPersistentEntitiesScriptEngine(QSharedPointer engine) { - std::lock_guard lock(_entitiesScriptEngineLock); - _nonPersistentEntitiesScriptEngine = engine; + _entitiesScriptEngine = engine; } void EntityScriptingInterface::callEntityMethod(const QUuid& id, const QString& method, const QStringList& params) { PROFILE_RANGE(script_entities, __FUNCTION__); - - 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); - } + + std::lock_guard lock(_entitiesScriptEngineLock); + if (_entitiesScriptEngine) { + EntityItemID entityID{ id }; + _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params); } } @@ -1107,13 +1099,9 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer params << paramString; } - 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()); - } + std::lock_guard lock(_entitiesScriptEngineLock); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params, senderNode->getUUID()); } } } @@ -1344,7 +1332,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 14d853fbaf..fca0dad871 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -181,8 +181,7 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } - void setPersistentEntitiesScriptEngine(QSharedPointer engine); - void setNonPersistentEntitiesScriptEngine(QSharedPointer engine); + void setEntitiesScriptEngine(QSharedPointer engine); void resetActivityTracking(); ActivityTracking getActivityTracking() const { return _activityTracking; } @@ -2511,12 +2510,9 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); protected: - 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); - } + void withEntitiesScriptEngine(std::function)> function) { + std::lock_guard lock(_entitiesScriptEngineLock); + function(_entitiesScriptEngine); }; private slots: @@ -2546,8 +2542,7 @@ private: EntityTreePointer _entityTree; std::recursive_mutex _entitiesScriptEngineLock; - QSharedPointer _persistentEntitiesScriptEngine; - QSharedPointer _nonPersistentEntitiesScriptEngine; + QSharedPointer _entitiesScriptEngine; bool _bidOnSimulationOwnership { false };