From 30480f126bf3d407b40e0f85f70234057bf1d588 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 15 Dec 2015 17:26:17 -0800 Subject: [PATCH 01/41] Improved idle to walk forward transition Hooked up a transition animation from idle to walk in the animation json. Also fixed a bug in the AnimBlendLinearMove which was preventing the interpolation into idle from being correct. --- .../defaultAvatar_full/avatar-animation.json | 35 ++++++++++++++++--- .../animation/src/AnimBlendLinearMove.cpp | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index ae470509be..d63942c510 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -415,10 +415,25 @@ "states": [ { "id": "idle", - "interpTarget": 15, - "interpDuration": 15, + "interpTarget": 10, + "interpDuration": 10, "transitions": [ - { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingForward", "state": "idleToWalkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" }, + { "var": "isAway", "state": "awayIntro" } + ] + }, + { + "id": "idleToWalkFwd", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "idleToWalkFwdOnDone", "state": "walkFwd" }, + { "var": "isNotMoving", "state": "idle" }, { "var": "isMovingBackward", "state": "walkBwd" }, { "var": "isMovingRight", "state": "strafeRight" }, { "var": "isMovingLeft", "state": "strafeLeft" }, @@ -429,7 +444,7 @@ }, { "id": "walkFwd", - "interpTarget": 6, + "interpTarget": 15, "interpDuration": 6, "transitions": [ { "var": "isNotMoving", "state": "idle" }, @@ -638,6 +653,18 @@ } ] }, + { + "id": "idleToWalkFwd", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims/idle_to_walk.fbx", + "startFrame": 1.0, + "endFrame": 19.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, { "id": "walkBwd", "type": "blendLinearMove", diff --git a/libraries/animation/src/AnimBlendLinearMove.cpp b/libraries/animation/src/AnimBlendLinearMove.cpp index 3be6a0f1b8..609b464512 100644 --- a/libraries/animation/src/AnimBlendLinearMove.cpp +++ b/libraries/animation/src/AnimBlendLinearMove.cpp @@ -122,5 +122,5 @@ void AnimBlendLinearMove::setCurrentFrameInternal(float frame) { auto clipNode = std::dynamic_pointer_cast(_children.front()); assert(clipNode); const float NUM_FRAMES = (clipNode->getEndFrame() - clipNode->getStartFrame()) + 1.0f; - _phase = fmodf(frame, NUM_FRAMES); + _phase = fmodf(frame / NUM_FRAMES, 1.0f); } From d3c57821c0338b70e068b01f2b5f6c9721609773 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Dec 2015 14:00:04 -0800 Subject: [PATCH 02/41] call a method on actions before each physics simulation step. use this to attempt to dejitter held objects --- interface/src/Application.cpp | 3 + interface/src/avatar/AvatarActionHold.cpp | 70 ++++++++++++++++++- interface/src/avatar/AvatarActionHold.h | 14 ++-- .../entities/src/EntityActionInterface.h | 2 + libraries/physics/src/CharacterController.cpp | 13 ++++ libraries/physics/src/CharacterController.h | 2 + libraries/physics/src/ObjectAction.h | 34 ++++----- libraries/physics/src/ObjectActionOffset.h | 10 +-- libraries/physics/src/ObjectActionSpring.h | 10 +-- libraries/physics/src/PhysicsEngine.cpp | 8 +++ libraries/physics/src/PhysicsEngine.h | 1 + 11 files changed, 134 insertions(+), 33 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f3291469cb..e28d1b2eac 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2989,6 +2989,9 @@ void Application::update(float deltaTime) { _physicsEngine->changeObjects(motionStates); myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->forEachAction([&](EntityActionPointer action) { + action->prepareForPhysicsSimulation(); + }); getEntities()->getTree()->withWriteLock([&] { _physicsEngine->stepSimulation(); diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index fab838aa68..87cf53143c 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -14,6 +14,7 @@ #include #include "avatar/AvatarManager.h" +#include "CharacterController.h" const uint16_t AvatarActionHold::holdVersion = 1; @@ -32,6 +33,57 @@ AvatarActionHold::~AvatarActionHold() { #endif } +void AvatarActionHold::prepareForPhysicsSimulation() { + auto avatarManager = DependencyManager::get(); + auto holdingAvatar = std::static_pointer_cast(avatarManager->getAvatarBySessionID(_holderID)); + + if (!holdingAvatar) { + return; + } + + withTryReadLock([&]{ + bool isRightHand = (_hand == "right"); + + if (_ignoreIK && holdingAvatar->isMyAvatar()) { + return; + } + + if (holdingAvatar->isMyAvatar()) { + glm::vec3 palmPosition { Vectors::ZERO }; + glm::quat palmRotation { Quaternions::IDENTITY }; + if (isRightHand) { + palmPosition = holdingAvatar->getHand()->getCopyOfPalmData(HandData::RightHand).getPosition(); + palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::RightHand).getRotation(); + } else { + palmPosition = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getPosition(); + palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getRotation(); + } + + // XXX dup code + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; + if (!controller) { + qDebug() << "AvatarActionHold::prepareForPhysicsSimulation failed to get character controller"; + return; + } + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + + + if (isRightHand) { + palmPosition = holdingAvatar->getRightPalmPosition(); + palmRotation = holdingAvatar->getRightPalmRotation(); + } else { + palmPosition = holdingAvatar->getLeftPalmPosition(); + palmRotation = holdingAvatar->getLeftPalmRotation(); + } + + _palmOffsetFromRigidBody = palmPosition - avatarRigidBodyPosition; + } + }); +} + std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::vec3& position) { auto avatarManager = DependencyManager::get(); auto holdingAvatar = std::static_pointer_cast(avatarManager->getAvatarBySessionID(_holderID)); @@ -44,7 +96,7 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve bool isRightHand = (_hand == "right"); glm::vec3 palmPosition { Vectors::ZERO }; glm::quat palmRotation { Quaternions::IDENTITY }; - + if (_ignoreIK && holdingAvatar->isMyAvatar()) { // We cannot ignore other avatars IK and this is not the point of this option // This is meant to make the grabbing behavior more reactive. @@ -55,6 +107,22 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve palmPosition = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getPosition(); palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getRotation(); } + } else if (holdingAvatar->isMyAvatar()) { + // XXX dup code + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; + if (!controller) { + qDebug() << "AvatarActionHold::prepareForPhysicsSimulation failed to get character controller"; + return; + } + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + + + + palmPosition = avatarRigidBodyPosition + _palmOffsetFromRigidBody; + palmRotation = holdingAvatar->getRightPalmRotation(); // XXX } else { if (isRightHand) { palmPosition = holdingAvatar->getRightPalmPosition(); diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index 63f30a75d9..2746c817ca 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -25,18 +25,20 @@ public: AvatarActionHold(const QUuid& id, EntityItemPointer ownerEntity); virtual ~AvatarActionHold(); - virtual bool updateArguments(QVariantMap arguments); - virtual QVariantMap getArguments(); + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; - virtual void updateActionWorker(float deltaTimeStep); + virtual void updateActionWorker(float deltaTimeStep) override; QByteArray serialize() const; - virtual void deserialize(QByteArray serializedArguments); + virtual void deserialize(QByteArray serializedArguments) override; - virtual bool shouldSuppressLocationEdits() { return _active && !_ownerEntity.expired(); } + virtual bool shouldSuppressLocationEdits() override { return _active && !_ownerEntity.expired(); } std::shared_ptr getTarget(glm::quat& rotation, glm::vec3& position); + virtual void prepareForPhysicsSimulation() override; + private: void doKinematicUpdate(float deltaTimeStep); @@ -56,6 +58,8 @@ private: float _previousDeltaTimeStep = 0.0f; glm::vec3 _previousPositionalDelta; + + glm::vec3 _palmOffsetFromRigidBody; }; #endif // hifi_AvatarActionHold_h diff --git a/libraries/entities/src/EntityActionInterface.h b/libraries/entities/src/EntityActionInterface.h index a192661e52..ba59d66cf4 100644 --- a/libraries/entities/src/EntityActionInterface.h +++ b/libraries/entities/src/EntityActionInterface.h @@ -58,6 +58,8 @@ public: virtual bool shouldSuppressLocationEdits() { return false; } + virtual void prepareForPhysicsSimulation() { } + // these look in the arguments map for a named argument. if it's not found or isn't well formed, // ok will be set to false (note that it's never set to true -- set it to true before calling these). // if required is true, failure to extract an argument will cause a warning to be printed. diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 44d4269e0e..86d57b7ee9 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -15,6 +15,7 @@ #include "BulletUtil.h" #include "PhysicsCollisionGroups.h" +#include "ObjectMotionState.h" const btVector3 LOCAL_UP_AXIS(0.0f, 1.0f, 0.0f); const float JUMP_SPEED = 3.5f; @@ -379,3 +380,15 @@ void CharacterController::preSimulation() { void CharacterController::postSimulation() { // postSimulation() exists for symmetry and just in case we need to do something here later } + + +bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) { + if (!_rigidBody) { + return false; + } + + const btTransform& worldTrans = _rigidBody->getCenterOfMassTransform(); + avatarRigidBodyPosition = bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset(); + avatarRigidBodyRotation = bulletToGLM(worldTrans.getRotation()); + return true; +} diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 88c02d0940..7bdc35fc0b 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -79,6 +79,8 @@ public: void setEnabled(bool enabled); bool isEnabled() const { return _enabled && _dynamicsWorld; } + bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); + protected: void updateUpAxis(const glm::quat& rotation); diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index e44036eadc..4ca13f2fbf 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -29,12 +29,12 @@ public: ObjectAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectAction(); - virtual void removeFromSimulation(EntitySimulation* simulation) const; - virtual EntityItemWeakPointer getOwnerEntity() const { return _ownerEntity; } - virtual void setOwnerEntity(const EntityItemPointer ownerEntity) { _ownerEntity = ownerEntity; } + virtual void removeFromSimulation(EntitySimulation* simulation) const override; + virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } + virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } - virtual bool updateArguments(QVariantMap arguments); - virtual QVariantMap getArguments(); + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; // this is called from updateAction and should be overridden by subclasses virtual void updateActionWorker(float deltaTimeStep) = 0; @@ -43,25 +43,25 @@ public: virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep); virtual void debugDraw(btIDebugDraw* debugDrawer); - virtual QByteArray serialize() const = 0; - virtual void deserialize(QByteArray serializedArguments) = 0; + virtual QByteArray serialize() const override = 0; + virtual void deserialize(QByteArray serializedArguments) override = 0; - virtual bool lifetimeIsOver(); - virtual quint64 getExpires() { return _expires; } + virtual bool lifetimeIsOver() override; + virtual quint64 getExpires() override { return _expires; } protected: quint64 localTimeToServerTime(quint64 timeValue) const; quint64 serverTimeToLocalTime(quint64 timeValue) const; virtual btRigidBody* getRigidBody(); - virtual glm::vec3 getPosition(); - virtual void setPosition(glm::vec3 position); - virtual glm::quat getRotation(); - virtual void setRotation(glm::quat rotation); - virtual glm::vec3 getLinearVelocity(); - virtual void setLinearVelocity(glm::vec3 linearVelocity); - virtual glm::vec3 getAngularVelocity(); - virtual void setAngularVelocity(glm::vec3 angularVelocity); + virtual glm::vec3 getPosition() override; + virtual void setPosition(glm::vec3 position) override; + virtual glm::quat getRotation() override; + virtual void setRotation(glm::quat rotation) override; + virtual glm::vec3 getLinearVelocity() override; + virtual void setLinearVelocity(glm::vec3 linearVelocity) override; + virtual glm::vec3 getAngularVelocity() override; + virtual void setAngularVelocity(glm::vec3 angularVelocity) override; virtual void activateBody(); virtual void forceBodyNonStatic(); diff --git a/libraries/physics/src/ObjectActionOffset.h b/libraries/physics/src/ObjectActionOffset.h index 1918da6996..ea01b10d33 100644 --- a/libraries/physics/src/ObjectActionOffset.h +++ b/libraries/physics/src/ObjectActionOffset.h @@ -22,13 +22,13 @@ public: ObjectActionOffset(const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectActionOffset(); - virtual bool updateArguments(QVariantMap arguments); - virtual QVariantMap getArguments(); + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; - virtual void updateActionWorker(float deltaTimeStep); + virtual void updateActionWorker(float deltaTimeStep) override; - virtual QByteArray serialize() const; - virtual void deserialize(QByteArray serializedArguments); + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; private: static const uint16_t offsetVersion; diff --git a/libraries/physics/src/ObjectActionSpring.h b/libraries/physics/src/ObjectActionSpring.h index caa64c3d3a..96bb900bf6 100644 --- a/libraries/physics/src/ObjectActionSpring.h +++ b/libraries/physics/src/ObjectActionSpring.h @@ -19,13 +19,13 @@ public: ObjectActionSpring(const QUuid& id, EntityItemPointer ownerEntity); virtual ~ObjectActionSpring(); - virtual bool updateArguments(QVariantMap arguments); - virtual QVariantMap getArguments(); + virtual bool updateArguments(QVariantMap arguments) override; + virtual QVariantMap getArguments() override; - virtual void updateActionWorker(float deltaTimeStep); + virtual void updateActionWorker(float deltaTimeStep) override; - virtual QByteArray serialize() const; - virtual void deserialize(QByteArray serializedArguments); + virtual QByteArray serialize() const override; + virtual void deserialize(QByteArray serializedArguments) override; protected: static const uint16_t springVersion; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 10e285186c..22695a1b66 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -497,3 +497,11 @@ void PhysicsEngine::removeAction(const QUuid actionID) { _objectActions.remove(actionID); } } + +void PhysicsEngine::forEachAction(std::function actor) { + QHashIterator iter(_objectActions); + while (iter.hasNext()) { + iter.next(); + actor(iter.value()); + } +} diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index e7b5fd79d4..05032ccae2 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -97,6 +97,7 @@ public: EntityActionPointer getActionByID(const QUuid& actionID) const; void addAction(EntityActionPointer action); void removeAction(const QUuid actionID); + void forEachAction(std::function actor); private: void removeContacts(ObjectMotionState* motionState); From 8bfbb69316712d5573aa435dc089c816de7f7c27 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Dec 2015 14:21:56 -0800 Subject: [PATCH 03/41] clean up code, change try-locks to locks --- interface/src/avatar/AvatarActionHold.cpp | 89 ++++++++--------------- interface/src/avatar/AvatarActionHold.h | 1 + 2 files changed, 31 insertions(+), 59 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 87cf53143c..32d35e3f8f 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -33,6 +33,19 @@ AvatarActionHold::~AvatarActionHold() { #endif } +glm::vec3 AvatarActionHold::getAvatarRigidBodyPosition() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; + if (!controller) { + qDebug() << "AvatarActionHold::getAvatarRigidBodyPosition failed to get character controller"; + return glm::vec3(0.0f); + } + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + return avatarRigidBodyPosition; +} + void AvatarActionHold::prepareForPhysicsSimulation() { auto avatarManager = DependencyManager::get(); auto holdingAvatar = std::static_pointer_cast(avatarManager->getAvatarBySessionID(_holderID)); @@ -41,45 +54,14 @@ void AvatarActionHold::prepareForPhysicsSimulation() { return; } - withTryReadLock([&]{ - bool isRightHand = (_hand == "right"); - + withReadLock([&]{ if (_ignoreIK && holdingAvatar->isMyAvatar()) { return; } - if (holdingAvatar->isMyAvatar()) { - glm::vec3 palmPosition { Vectors::ZERO }; - glm::quat palmRotation { Quaternions::IDENTITY }; - if (isRightHand) { - palmPosition = holdingAvatar->getHand()->getCopyOfPalmData(HandData::RightHand).getPosition(); - palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::RightHand).getRotation(); - } else { - palmPosition = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getPosition(); - palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getRotation(); - } - - // XXX dup code - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; - if (!controller) { - qDebug() << "AvatarActionHold::prepareForPhysicsSimulation failed to get character controller"; - return; - } - glm::vec3 avatarRigidBodyPosition; - glm::quat avatarRigidBodyRotation; - controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); - - - if (isRightHand) { - palmPosition = holdingAvatar->getRightPalmPosition(); - palmRotation = holdingAvatar->getRightPalmRotation(); - } else { - palmPosition = holdingAvatar->getLeftPalmPosition(); - palmRotation = holdingAvatar->getLeftPalmRotation(); - } - - _palmOffsetFromRigidBody = palmPosition - avatarRigidBodyPosition; + glm::vec3 palmPosition = (_hand == "right") ? + holdingAvatar->getRightPalmPosition() : holdingAvatar->getLeftPalmPosition(); + _palmOffsetFromRigidBody = palmPosition - getAvatarRigidBodyPosition(); } }); } @@ -92,7 +74,7 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve return holdingAvatar; } - withTryReadLock([&]{ + withReadLock([&]{ bool isRightHand = (_hand == "right"); glm::vec3 palmPosition { Vectors::ZERO }; glm::quat palmRotation { Quaternions::IDENTITY }; @@ -108,21 +90,12 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getRotation(); } } else if (holdingAvatar->isMyAvatar()) { - // XXX dup code - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; - if (!controller) { - qDebug() << "AvatarActionHold::prepareForPhysicsSimulation failed to get character controller"; - return; + palmPosition = getAvatarRigidBodyPosition() + _palmOffsetFromRigidBody; + if (isRightHand) { + palmRotation = holdingAvatar->getRightPalmRotation(); + } else { + palmRotation = holdingAvatar->getLeftPalmRotation(); } - glm::vec3 avatarRigidBodyPosition; - glm::quat avatarRigidBodyRotation; - controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); - - - - palmPosition = avatarRigidBodyPosition + _palmOffsetFromRigidBody; - palmRotation = holdingAvatar->getRightPalmRotation(); // XXX } else { if (isRightHand) { palmPosition = holdingAvatar->getRightPalmPosition(); @@ -171,21 +144,19 @@ void AvatarActionHold::updateActionWorker(float deltaTimeStep) { if (valid && holdCount > 0) { position /= holdCount; - bool gotLock = withTryWriteLock([&]{ + withWriteLock([&]{ _positionalTarget = position; _rotationalTarget = rotation; _positionalTargetSet = true; _rotationalTargetSet = true; _active = true; }); - if (gotLock) { - if (_kinematic) { - doKinematicUpdate(deltaTimeStep); - } else { - activateBody(); - forceBodyNonStatic(); - ObjectActionSpring::updateActionWorker(deltaTimeStep); - } + if (_kinematic) { + doKinematicUpdate(deltaTimeStep); + } else { + activateBody(); + forceBodyNonStatic(); + ObjectActionSpring::updateActionWorker(deltaTimeStep); } } } diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index 2746c817ca..36a9aa3049 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -35,6 +35,7 @@ public: virtual bool shouldSuppressLocationEdits() override { return _active && !_ownerEntity.expired(); } + glm::vec3 getAvatarRigidBodyPosition(); std::shared_ptr getTarget(glm::quat& rotation, glm::vec3& position); virtual void prepareForPhysicsSimulation() override; From c14433979b1911a51afbbf6cdcfddf30be4379ed Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Dec 2015 15:09:37 -0800 Subject: [PATCH 04/41] fix crash mode when model late to get collision URL --- .../src/RenderableModelEntityItem.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index b5203ea460..83bfbf7f05 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -385,7 +385,7 @@ Model* RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { _needsInitialSimulation = true; } } - + return result; } @@ -398,14 +398,14 @@ void RenderableModelEntityItem::update(const quint64& now) { EntityItemProperties properties; auto extents = _model->getMeshExtents(); properties.setDimensions(extents.maximum - extents.minimum); - + qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL()); QMetaObject::invokeMethod(DependencyManager::get().data(), "editEntity", Qt::QueuedConnection, Q_ARG(QUuid, getEntityItemID()), Q_ARG(EntityItemProperties, properties)); } - + ModelEntityItem::update(now); } @@ -427,7 +427,7 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori // << precisionPicking; QString extraInfo; - return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, + return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo, precisionPicking); } @@ -447,24 +447,22 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { ShapeType type = getShapeType(); if (type == SHAPE_TYPE_COMPOUND) { - if (!_model) { + if (!_model || _model->getCollisionURL().isEmpty()) { EntityTreePointer tree = getTree(); if (tree) { QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); } - return false; // hmm... + return false; } - assert(!_model->getCollisionURL().isEmpty()); - if (_model->getURL().isEmpty()) { // we need a render geometry with a scale to proceed, so give up. return false; } - + const QSharedPointer collisionNetworkGeometry = _model->getCollisionGeometry(); const QSharedPointer renderNetworkGeometry = _model->getGeometry(); - + if ((collisionNetworkGeometry && collisionNetworkGeometry->isLoaded()) && (renderNetworkGeometry && renderNetworkGeometry->isLoaded())) { // we have both URLs AND both geometries AND they are both fully loaded. From ff83f8fc37081bfb115cb8878ff224235eed4563 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Dec 2015 15:10:30 -0800 Subject: [PATCH 05/41] fix crash mode when leaving domain with handgun --- libraries/physics/src/PhysicalEntitySimulation.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 40366257df..5a12627abd 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -44,7 +44,7 @@ void PhysicalEntitySimulation::updateEntitiesInternal(const quint64& now) { void PhysicalEntitySimulation::addEntityInternal(EntityItemPointer entity) { assert(entity); - if (entity->shouldBePhysical()) { + if (entity->shouldBePhysical()) { EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); if (!motionState) { _pendingAdds.insert(entity); @@ -117,6 +117,7 @@ void PhysicalEntitySimulation::clearEntitiesInternal() { _pendingRemoves.clear(); _pendingAdds.clear(); _pendingChanges.clear(); + _outgoingChanges.clear(); } // end EntitySimulation overrides From 489d4099a7dbd76f59fa2d7b62092de402ba5748 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Dec 2015 15:21:20 -0800 Subject: [PATCH 06/41] add locationChanged to model entities --- interface/src/avatar/AvatarActionHold.cpp | 2 ++ .../entities-renderer/src/RenderableModelEntityItem.cpp | 8 ++++++++ .../entities-renderer/src/RenderableModelEntityItem.h | 1 + 3 files changed, 11 insertions(+) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 32d35e3f8f..160a437f44 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -199,6 +199,8 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { motionState->dirtyInternalKinematicChanges(); + ownerEntity->setPosition(_positionalTarget); + _previousPositionalTarget = _positionalTarget; _previousRotationalTarget = _rotationalTarget; _previousSet = true; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index b5203ea460..14e89b59fd 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -625,3 +625,11 @@ glm::vec3 RenderableModelEntityItem::getAbsoluteJointTranslationInObjectFrame(in } return glm::vec3(0.0f); } + +void RenderableModelEntityItem::locationChanged() { + EntityItem::locationChanged(); + if (_model && _model->isActive()) { + _model->setRotation(getRotation()); + _model->setTranslation(getPosition()); + } +} diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 143113146e..cf948bd7a0 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -73,6 +73,7 @@ public: virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; virtual void loader() override; + virtual void locationChanged() override; private: void remapTextures(); From 45e31ca4422359e6af92ba257fdb20bbaa1469b6 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 16 Dec 2015 15:35:23 -0800 Subject: [PATCH 07/41] fix warning about unused variable --- interface/src/avatar/Avatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f8040754d7..6094a0b9fe 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -157,7 +157,7 @@ void Avatar::animateScaleChanges(float deltaTime) { // snap to the end when we get close enough const float MIN_RELATIVE_SCALE_ERROR = 0.03f; - if (fabsf(_targetScale - currentScale) / _targetScale < 0.03f) { + if (fabsf(_targetScale - currentScale) / _targetScale < MIN_RELATIVE_SCALE_ERROR) { animatedScale = _targetScale; } From d7affcf811a8e5a46d5f201336effbeeec1584e3 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Dec 2015 15:41:27 -0800 Subject: [PATCH 08/41] keep track of palm rotation vs avatar as well as palm translation --- interface/src/avatar/AvatarActionHold.cpp | 49 +++++++++++++++++------ interface/src/avatar/AvatarActionHold.h | 3 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 160a437f44..5a4dbfa4d1 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -33,17 +33,15 @@ AvatarActionHold::~AvatarActionHold() { #endif } -glm::vec3 AvatarActionHold::getAvatarRigidBodyPosition() { +bool AvatarActionHold::getAvatarRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation) { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); MyCharacterController* controller = myAvatar ? myAvatar->getCharacterController() : nullptr; if (!controller) { - qDebug() << "AvatarActionHold::getAvatarRigidBodyPosition failed to get character controller"; - return glm::vec3(0.0f); + qDebug() << "AvatarActionHold::getAvatarRigidBodyLocation failed to get character controller"; + return false; } - glm::vec3 avatarRigidBodyPosition; - glm::quat avatarRigidBodyRotation; controller->getRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); - return avatarRigidBodyPosition; + return true; } void AvatarActionHold::prepareForPhysicsSimulation() { @@ -59,9 +57,28 @@ void AvatarActionHold::prepareForPhysicsSimulation() { return; } if (holdingAvatar->isMyAvatar()) { - glm::vec3 palmPosition = (_hand == "right") ? - holdingAvatar->getRightPalmPosition() : holdingAvatar->getLeftPalmPosition(); - _palmOffsetFromRigidBody = palmPosition - getAvatarRigidBodyPosition(); + glm::vec3 palmPosition; + glm::quat palmRotation; + if (_hand == "right") { + palmPosition = holdingAvatar->getRightPalmPosition(); + palmRotation = holdingAvatar->getRightPalmRotation(); + } else { + palmPosition = holdingAvatar->getLeftPalmPosition(); + palmRotation = holdingAvatar->getLeftPalmRotation(); + } + + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + getAvatarRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + + // determine the difference in translation and rotation between the avatar's + // rigid body and the palm position. The avatar's rigid body will be moved by bullet + // between this call and the call to getTarget, below. A call to get*PalmPosition in + // getTarget would get the palm position of the previous location of the avatar (because + // bullet has moved the av's rigid body but the rigid body's location has not yet been + // copied out into the Avatar class. + _palmOffsetFromRigidBody = palmPosition - avatarRigidBodyPosition; + _palmRotationFromRigidBody = glm::inverse(avatarRigidBodyRotation) * palmRotation; } }); } @@ -90,7 +107,17 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve palmRotation = holdingAvatar->getHand()->getCopyOfPalmData(HandData::LeftHand).getRotation(); } } else if (holdingAvatar->isMyAvatar()) { - palmPosition = getAvatarRigidBodyPosition() + _palmOffsetFromRigidBody; + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + getAvatarRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + + // the offset and rotation between the avatar's rigid body and the palm were determined earlier + // in prepareForPhysicsSimulation. At this point, the avatar's rigid body has been moved by bullet + // and the data in the Avatar class is stale. This means that the result of get*PalmPosition will + // be stale. Instead, determine the current palm position with the current avatar's rigid body + // location and the saved offsets. + palmPosition = avatarRigidBodyPosition + _palmOffsetFromRigidBody; + palmRotation = avatarRigidBodyRotation * _palmRotationFromRigidBody; if (isRightHand) { palmRotation = holdingAvatar->getRightPalmRotation(); } else { @@ -199,8 +226,6 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { motionState->dirtyInternalKinematicChanges(); - ownerEntity->setPosition(_positionalTarget); - _previousPositionalTarget = _positionalTarget; _previousRotationalTarget = _rotationalTarget; _previousSet = true; diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index 36a9aa3049..fc8baf6dcc 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -35,7 +35,7 @@ public: virtual bool shouldSuppressLocationEdits() override { return _active && !_ownerEntity.expired(); } - glm::vec3 getAvatarRigidBodyPosition(); + bool getAvatarRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); std::shared_ptr getTarget(glm::quat& rotation, glm::vec3& position); virtual void prepareForPhysicsSimulation() override; @@ -61,6 +61,7 @@ private: glm::vec3 _previousPositionalDelta; glm::vec3 _palmOffsetFromRigidBody; + glm::quat _palmRotationFromRigidBody; }; #endif // hifi_AvatarActionHold_h From 5f9bdcb2aa6ba705ac3173cdebf9fbb171e03b90 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Dec 2015 15:52:20 -0800 Subject: [PATCH 09/41] fix lock type --- interface/src/avatar/AvatarActionHold.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 5a4dbfa4d1..34b959575d 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -52,7 +52,7 @@ void AvatarActionHold::prepareForPhysicsSimulation() { return; } - withReadLock([&]{ + withWriteLock([&]{ if (_ignoreIK && holdingAvatar->isMyAvatar()) { return; } From fa5bab08b15effee1ff91cc5cc6a4934c4f2bbbf Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Dec 2015 16:03:30 -0800 Subject: [PATCH 10/41] Support web content inside QML --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/html/eventBridgeLoader.js | 59 ++ examples/html/qmlWebTest.html | 31 + examples/tests/qmlWebTest.js | 32 + interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 45 ++ interface/resources/qml/TestMenu.qml | 109 --- interface/resources/qml/WebEntity.qml | 6 +- interface/resources/qml/qwebchannel.js | 413 ++++++++++++ interface/src/Application.cpp | 18 + libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 344 ++++++++++ libraries/ui/src/QmlWebWindowClass.h | 92 +++ .../ui/src/impl/websocketclientwrapper.cpp | 72 ++ .../ui/src/impl/websocketclientwrapper.h | 63 ++ libraries/ui/src/impl/websockettransport.cpp | 100 +++ libraries/ui/src/impl/websockettransport.h | 60 ++ tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 622 +++++++++++++----- 24 files changed, 1878 insertions(+), 327 deletions(-) create mode 100644 examples/html/eventBridgeLoader.js create mode 100644 examples/html/qmlWebTest.html create mode 100644 examples/tests/qmlWebTest.js create mode 100644 interface/resources/qml/QmlWebWindow.qml create mode 100644 interface/resources/qml/qwebchannel.js create mode 100644 libraries/ui/src/QmlWebWindowClass.cpp create mode 100644 libraries/ui/src/QmlWebWindowClass.h create mode 100644 libraries/ui/src/impl/websocketclientwrapper.cpp create mode 100644 libraries/ui/src/impl/websocketclientwrapper.h create mode 100644 libraries/ui/src/impl/websockettransport.cpp create mode 100644 libraries/ui/src/impl/websockettransport.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index bb0b268dd4..17b5d5f49d 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js new file mode 100644 index 0000000000..b62e7d9384 --- /dev/null +++ b/examples/html/eventBridgeLoader.js @@ -0,0 +1,59 @@ + +//public slots: +// void emitWebEvent(const QString& data); +// void emitScriptEvent(const QString& data); +// +//signals: +// void webEventReceived(const QString& data); +// void scriptEventReceived(const QString& data); +// + +EventBridgeConnectionProxy = function(parent) { + this.parent = parent; + this.realSignal = this.parent.realBridge.scriptEventReceived + this.webWindowId = this.parent.webWindow.windowId; +} + +EventBridgeConnectionProxy.prototype.connect = function(callback) { + var that = this; + this.realSignal.connect(function(id, message) { + if (id === that.webWindowId) { callback(message); } + }); +} + +EventBridgeProxy = function(webWindow) { + this.webWindow = webWindow; + this.realBridge = this.webWindow.eventBridge; + this.scriptEventReceived = new EventBridgeConnectionProxy(this); +} + +EventBridgeProxy.prototype.emitWebEvent = function(data) { + this.realBridge.emitWebEvent(data); +} + +openEventBridge = function(callback) { + EVENT_BRIDGE_URI = "ws://localhost:51016"; + socket = new WebSocket(this.EVENT_BRIDGE_URI); + + socket.onclose = function() { + console.error("web channel closed"); + }; + + socket.onerror = function(error) { + console.error("web channel error: " + error); + }; + + socket.onopen = function() { + channel = new QWebChannel(socket, function(channel) { + console.log("Document url is " + document.URL); + for(var key in channel.objects){ + console.log("registered object: " + key); + } + var webWindow = channel.objects[document.URL.toLowerCase()]; + console.log("WebWindow is " + webWindow) + eventBridgeProxy = new EventBridgeProxy(webWindow); + if (callback) { callback(eventBridgeProxy); } + }); + } +} + diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html new file mode 100644 index 0000000000..e59535701d --- /dev/null +++ b/examples/html/qmlWebTest.html @@ -0,0 +1,31 @@ + + +Properties + + + + + + + + + + + + + diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js new file mode 100644 index 0000000000..250852abe7 --- /dev/null +++ b/examples/tests/qmlWebTest.js @@ -0,0 +1,32 @@ +print("Launching web window"); + +webWindow = new QmlWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +print("JS Side window: " + webWindow); +print("JS Side bridge: " + webWindow.eventBridge); +webWindow.eventBridge.webEventReceived.connect(function(data) { + print("JS Side event received: " + data); +}); + +var titles = ["A", "B", "C"]; +var titleIndex = 0; + +Script.setInterval(function() { + webWindow.eventBridge.emitScriptEvent("JS Event sent"); + var size = webWindow.size; + var position = webWindow.position; + print("Window url: " + webWindow.url) + print("Window visible: " + webWindow.visible) + print("Window size: " + size.x + "x" + size.y) + print("Window pos: " + position.x + "x" + position.y) + webWindow.setVisible(!webWindow.visible); + webWindow.setTitle(titles[titleIndex]); + webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); + titleIndex += 1; + titleIndex %= titles.length; +}, 2 * 1000); + +Script.setTimeout(function() { + print("Closing script"); + webWindow.close(); + Script.stop(); +}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87cf1a384c..1d9557a835 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,7 +45,9 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS + Gui Multimedia Network OpenGL Qml Quick Script Svg + WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -175,9 +177,17 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL + Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg + Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets ) +# Issue causes build failure unless we add this directory. +# See https://bugreports.qt.io/browse/QTBUG-43351 +if (WIN32) + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) +endif() + # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -209,5 +219,9 @@ else (APPLE) endif() endif (APPLE) +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") +endif() + package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 947bf739fc..8f3cd0e1e7 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" import "styles" @@ -39,9 +39,10 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: scrollView.top + anchors.bottom: webview.top color: "white" } + Row { id: buttons spacing: 4 @@ -112,26 +113,22 @@ VrDialog { } } - ScrollView { - id: scrollView + WebEngineView { + id: webview + url: "http://highfidelity.com" anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - WebView { - id: webview - url: "http://highfidelity.com" - anchors.fill: parent - onLoadingChanged: { - if (loadRequest.status == WebView.LoadSucceededStarted) { - addressBar.text = loadRequest.url - } - } - onIconChanged: { - barIcon.source = icon + onLoadingChanged: { + if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + addressBar.text = loadRequest.url } } + onIconChanged: { + console.log("New icon: " + icon) + } } } // item @@ -146,5 +143,4 @@ VrDialog { break; } } - } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 012f04f1fd..75b82342ca 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -18,15 +18,11 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - ScrollView { + WebEngineView { + id: webview + objectName: "WebView" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: infoView.url - anchors.fill: parent - } - } - + url: infoView.url + } } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 946f32e84a..3a66c5340f 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -24,27 +24,22 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - - ScrollView { + WebEngineView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" - anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; - return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; return; - } + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; + return; } - } - - } + } + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml new file mode 100644 index 0000000000..d59ef4955e --- /dev/null +++ b/interface/resources/qml/QmlWebWindow.qml @@ -0,0 +1,45 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtWebSockets 1.0 + +import "qwebchannel.js" as WebChannel +import "controls" +import "styles" + +VrDialog { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + backgroundColor: "#7f000000" + property url source: "about:blank" + + Component.onCompleted: { + enabled = true + console.log("Web Window Created " + root); + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + } + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + + WebEngineView { + id: webview + url: root.source + anchors.fill: parent + } + } // item +} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index 4d109e6298..fe8a26e234 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,116 +4,7 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { - Item { - objectName: "AllActions" - Action { - id: aboutApp - objectName: "HifiAction_" + MenuConstants.AboutApp - text: qsTr("About Interface") - } - - // - // File Menu - // - Action { - id: login - objectName: "HifiAction_" + MenuConstants.Login - text: qsTr("Login") - } - Action { - id: quit - objectName: "HifiAction_" + MenuConstants.Quit - text: qsTr("Quit") - //shortcut: StandardKey.Quit - shortcut: "Ctrl+Q" - } - - - // - // Edit menu - // - Action { - id: undo - text: "Undo" - shortcut: StandardKey.Undo - } - - Action { - id: redo - text: "Redo" - shortcut: StandardKey.Redo - } - - Action { - id: animations - objectName: "HifiAction_" + MenuConstants.Animations - text: qsTr("Animations...") - } - Action { - id: attachments - text: qsTr("Attachments...") - } - Action { - id: explode - text: qsTr("Explode on quit") - checkable: true - checked: true - } - Action { - id: freeze - text: qsTr("Freeze on quit") - checkable: true - checked: false - } - ExclusiveGroup { - Action { - id: visibleToEveryone - objectName: "HifiAction_" + MenuConstants.VisibleToEveryone - text: qsTr("Everyone") - checkable: true - checked: true - } - Action { - id: visibleToFriends - objectName: "HifiAction_" + MenuConstants.VisibleToFriends - text: qsTr("Friends") - checkable: true - } - Action { - id: visibleToNoOne - objectName: "HifiAction_" + MenuConstants.VisibleToNoOne - text: qsTr("No one") - checkable: true - } - } - } - Menu { objectName: "rootMenu"; - Menu { - title: "File" - MenuItem { action: login } - MenuItem { action: explode } - MenuItem { action: freeze } - MenuItem { action: quit } - } - Menu { - title: "Tools" - Menu { - title: "I Am Visible To" - MenuItem { action: visibleToEveryone } - MenuItem { action: visibleToFriends } - MenuItem { action: visibleToNoOne } - } - MenuItem { action: animations } - } - Menu { - title: "Long menu name top menu" - MenuItem { action: aboutApp } - } - Menu { - title: "Help" - MenuItem { action: aboutApp } - } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index 0eb943cac7..ae94105672 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 -WebView { +WebEngineView { id: root - objectName: "webview" anchors.fill: parent + objectName: "webview" url: "about:blank" } diff --git a/interface/resources/qml/qwebchannel.js b/interface/resources/qml/qwebchannel.js new file mode 100644 index 0000000000..d8c28bc663 --- /dev/null +++ b/interface/resources/qml/qwebchannel.js @@ -0,0 +1,413 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + for (var i in message.data) { + var data = message.data[i]; + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); + } + // now unwrap properties, which might reference other registered objects + for (var objectName in channel.objects) { + channel.objects[objectName].unwrapProperties(); + } + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + var ret = new Array(response.length); + for (var i = 0; i < response.length; ++i) { + ret[i] = object.unwrapQObject(response[i]); + } + return ret; + } + if (!response + || !response["__QObject*__"] + || response.id === undefined) { + return response; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + var propertyNames = []; + for (var propertyName in qObject) { + propertyNames.push(propertyName); + } + for (var idx in propertyNames) { + delete qObject[propertyNames[idx]]; + } + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (var propertyIdx in object.__propertyCache__) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal && signalName !== "destroyed") { + // only required for "pure" signals, handled separately for properties in propertyUpdate + // also note that we always get notified about the destroyed signal + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyIndex in propertyMap) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, signalArgs); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") + callback = arguments[i]; + else + args.push(arguments[i]); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": methodIdx, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": value + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 67b1be9c4d..f0d41f0426 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,7 @@ #include #include #include +#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -362,6 +363,17 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); + Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -623,6 +635,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(_glWidget->context()->contextHandle()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); @@ -4138,6 +4155,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); + scriptEngine->registerFunction("QmlWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index bddc7d19ae..26a564e20b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,10 +320,13 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; - } + return; + } + + + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -437,7 +440,9 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { + QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); + _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 67315b0783..d66cbeb285 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 140ca87d0d..cc2382926f 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp new file mode 100644 index 0000000000..3797034e79 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -0,0 +1,344 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QmlWebWindowClass.h" + +#include + +#include +#include +#include + +#include + +#include +#include + +#include + +#include "impl/websocketclientwrapper.h" +#include "impl/websockettransport.h" +#include "OffscreenUi.h" + +static QWebSocketServer * webChannelServer { nullptr }; +static WebSocketClientWrapper * webChannelClientWrapper { nullptr }; +static QWebChannel webChannel; +static std::once_flag webChannelSetup; +static const uint16_t WEB_CHANNEL_PORT = 51016; +static std::atomic nextWindowId; + +void initWebChannelServer() { + std::call_once(webChannelSetup, [] { + webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + webChannelClientWrapper = new WebSocketClientWrapper(webChannelServer); + if (!webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + QObject::connect(webChannelClientWrapper, &WebSocketClientWrapper::clientConnected, &webChannel, &QWebChannel::connectTo); + }); +} + +void QmlScriptEventBridge::emitWebEvent(const QString& data) { + QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); +} + +void QmlScriptEventBridge::emitScriptEvent(const QString& data) { + QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, + Q_ARG(int, _webWindow->getWindowId()), + Q_ARG(QString, data) + ); +} + +QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + QmlWebWindowClass* retVal { nullptr }; + const QString title = context->argument(0).toString(); + QString url = context->argument(1).toString(); + if (!url.startsWith("http") && !url.startsWith("file://")) { + url = QUrl::fromLocalFile(url).toString(); + } + const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));; + const int height = std::max(100, std::min(720, context->argument(3).toInt32()));; + + // Build the event bridge and wrapper on the main thread + QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, "QmlWebWindow.qml"), + Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + initWebChannelServer(); + retVal = new QmlWebWindowClass(object); + webChannel.registerObject(url.toLower(), retVal); + retVal->setTitle(title); + retVal->setURL(url); + retVal->setSize(width, height); + }) + ); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); + return engine->newQObject(retVal); +} + +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) + : _isToolWindow(false), _windowId(++nextWindowId), _eventBridge(new QmlScriptEventBridge(this)), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); +} + +void QmlWebWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); + return; + } + + auto qmlWindow = (QQuickItem*)_qmlWindow; + if (qmlWindow->isEnabled() != visible) { + qmlWindow->setEnabled(visible); + emit visibilityChanged(visible); + } +} + +bool QmlWebWindowClass::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + return result; + } + + return ((QQuickItem*)_qmlWindow)->isEnabled(); +} + + +glm::vec2 QmlWebWindowClass::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(((QQuickItem*)_qmlWindow)->x(), ((QQuickItem*)_qmlWindow)->y()); +} + + +void QmlWebWindowClass::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); + return; + } + + ((QQuickItem*)_qmlWindow)->setPosition(QPointF(position.x, position.y)); +} + +void QmlWebWindowClass::setPosition(int x, int y) { + setPosition(glm::vec2(x, y)); +} + +glm::vec2 QmlWebWindowClass::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(((QQuickItem*)_qmlWindow)->width(), ((QQuickItem*)_qmlWindow)->height()); +} + +void QmlWebWindowClass::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); + } + + ((QQuickItem*)_qmlWindow)->setSize(QSizeF(size.x, size.y)); +} + +void QmlWebWindowClass::setSize(int width, int height) { + setSize(glm::vec2(width, height)); +} + +static const char* const URL_PROPERTY = "source"; + +QString QmlWebWindowClass::getURL() const { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); + return result; + } + return _qmlWindow->property(URL_PROPERTY).toString(); +} + +void QmlWebWindowClass::setURL(const QString& urlString) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); + } + _qmlWindow->setProperty(URL_PROPERTY, urlString); +} + +static const char* const TITLE_PROPERTY = "title"; + +void QmlWebWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); + } + + _qmlWindow->setProperty(TITLE_PROPERTY, title); +} + +void QmlWebWindowClass::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); + } + _qmlWindow->setProperty("destroyOnInvisible", true); + _qmlWindow->setProperty("visible", false); + _qmlWindow->deleteLater(); +} + +void QmlWebWindowClass::hasClosed() { +} + +void QmlWebWindowClass::raise() { +} + +#if 0 + +#include + +#include + +WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) + : QObject(NULL), _eventBridge(new ScriptEventBridge(this)), _isToolWindow(isToolWindow) { + /* + if (_isToolWindow) { + ToolWindow* toolWindow = qApp->getToolWindow(); + + auto dockWidget = new QDockWidget(title, toolWindow); + dockWidget->setFeatures(QDockWidget::DockWidgetMovable); + connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged); + + _webView = new QWebView(dockWidget); + addEventBridgeToWindowObject(); + + dockWidget->setWidget(_webView); + + auto titleWidget = new QWidget(dockWidget); + dockWidget->setTitleBarWidget(titleWidget); + + toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal); + + _windowWidget = dockWidget; + } else { + auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); + dialogWidget->setWindowTitle(title); + dialogWidget->resize(width, height); + dialogWidget->installEventFilter(this); + connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); + + auto layout = new QVBoxLayout(dialogWidget); + layout->setContentsMargins(0, 0, 0, 0); + dialogWidget->setLayout(layout); + + _webView = new QWebView(dialogWidget); + + layout->addWidget(_webView); + + addEventBridgeToWindowObject(); + + _windowWidget = dialogWidget; + } + + auto style = QStyleFactory::create("fusion"); + if (style) { + _webView->setStyle(style); + } + + _webView->setPage(new DataWebPage()); + if (!url.startsWith("http") && !url.startsWith("file://")) { + _webView->setUrl(QUrl::fromLocalFile(url)); + } else { + _webView->setUrl(url); + } + connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); + connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, + this, &WebWindowClass::addEventBridgeToWindowObject); + */ +} + +void WebWindowClass::hasClosed() { + emit closed(); +} + + +void WebWindowClass::setVisible(bool visible) { +} + +QString WebWindowClass::getURL() const { + return QString(); +} + +void WebWindowClass::setURL(const QString& url) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::AutoConnection, Q_ARG(QString, url)); + return; + } +} + +QSizeF WebWindowClass::getSize() const { + QSizeF size; + return size; +} + +void WebWindowClass::setSize(const QSizeF& size) { + setSize(size.width(), size.height()); +} + +void WebWindowClass::setSize(int width, int height) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::AutoConnection, Q_ARG(int, width), Q_ARG(int, height)); + return; + } +} + +glm::vec2 WebWindowClass::getPosition() const { + return glm::vec2(); +} + +void WebWindowClass::setPosition(const glm::vec2& position) { + setPosition(position.x, position.y); +} + +void WebWindowClass::setPosition(int x, int y) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::AutoConnection, Q_ARG(int, x), Q_ARG(int, y)); + return; + } +} + +void WebWindowClass::raise() { +} + +QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + WebWindowClass* retVal { nullptr }; + //QString file = context->argument(0).toString(); + //QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, + // Q_RETURN_ARG(WebWindowClass*, retVal), + // Q_ARG(const QString&, file), + // Q_ARG(QString, context->argument(1).toString()), + // Q_ARG(int, context->argument(2).toInteger()), + // Q_ARG(int, context->argument(3).toInteger()), + // Q_ARG(bool, context->argument(4).toBool())); + + //connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); + + return engine->newQObject(retVal); +} + +void WebWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::AutoConnection, Q_ARG(QString, title)); + return; + } +} + +#endif \ No newline at end of file diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h new file mode 100644 index 0000000000..c166fd2c20 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -0,0 +1,92 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ui_QmlWebWindowClass_h +#define hifi_ui_QmlWebWindowClass_h + +#include +#include +#include +#include + +class QScriptEngine; +class QScriptContext; +class QmlWebWindowClass; + +class QmlScriptEventBridge : public QObject { + Q_OBJECT +public: + QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {} + +public slots : + void emitWebEvent(const QString& data); + void emitScriptEvent(const QString& data); + +signals: + void webEventReceived(const QString& data); + void scriptEventReceived(int windowId, const QString& data); + +private: + const QmlWebWindowClass* _webWindow { nullptr }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWebWindowClass : public QObject { + Q_OBJECT + Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) + Q_PROPERTY(int windowId READ getWindowId CONSTANT) + Q_PROPERTY(QString url READ getURL CONSTANT) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) + +public: + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + QmlWebWindowClass(QObject* qmlWindow); + +public slots: + bool isVisible() const; + void setVisible(bool visible); + + glm::vec2 getPosition() const; + void setPosition(const glm::vec2& position); + void setPosition(int x, int y); + + glm::vec2 getSize() const; + void setSize(const glm::vec2& size); + void setSize(int width, int height); + + QString getURL() const; + void setURL(const QString& url); + + void setTitle(const QString& title); + + // Ugh.... do not want to do + Q_INVOKABLE void raise(); + Q_INVOKABLE void close(); + Q_INVOKABLE int getWindowId() const { return _windowId; }; + Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; + +signals: + void visibilityChanged(bool visible); // Tool window + void urlChanged(); + void moved(glm::vec2 position); + void resized(QSizeF size); + void closed(); + +private slots: + void hasClosed(); + +private: + QmlScriptEventBridge* _eventBridge; + bool _isToolWindow { false }; + QObject* _qmlWindow; + const int _windowId; +}; + +#endif diff --git a/libraries/ui/src/impl/websocketclientwrapper.cpp b/libraries/ui/src/impl/websocketclientwrapper.cpp new file mode 100644 index 0000000000..00ddd6e009 --- /dev/null +++ b/libraries/ui/src/impl/websocketclientwrapper.cpp @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "websocketclientwrapper.h" + +#include + +#include "websockettransport.h" + +/*! + \brief Wrapps connected QWebSockets clients in WebSocketTransport objects. + + This code is all that is required to connect incoming WebSockets to the WebChannel. Any kind + of remote JavaScript client that supports WebSockets can thus receive messages and access the + published objects. +*/ + +QT_BEGIN_NAMESPACE + +/*! + Construct the client wrapper with the given parent. + + All clients connecting to the QWebSocketServer will be automatically wrapped + in WebSocketTransport objects. +*/ +WebSocketClientWrapper::WebSocketClientWrapper(QWebSocketServer *server, QObject *parent) + : QObject(parent) + , m_server(server) +{ + connect(server, &QWebSocketServer::newConnection, + this, &WebSocketClientWrapper::handleNewConnection); +} + +/*! + Wrap an incoming WebSocket connection in a WebSocketTransport object. +*/ +void WebSocketClientWrapper::handleNewConnection() +{ + emit clientConnected(new WebSocketTransport(m_server->nextPendingConnection())); +} + +QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websocketclientwrapper.h b/libraries/ui/src/impl/websocketclientwrapper.h new file mode 100644 index 0000000000..edb0a1b1a3 --- /dev/null +++ b/libraries/ui/src/impl/websocketclientwrapper.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBSOCKETTRANSPORTSERVER_H +#define WEBSOCKETTRANSPORTSERVER_H + +#include + +QT_BEGIN_NAMESPACE + +class QWebSocketServer; +class WebSocketTransport; + +class WebSocketClientWrapper : public QObject +{ + Q_OBJECT + +public: + WebSocketClientWrapper(QWebSocketServer *server, QObject *parent = 0); + +Q_SIGNALS: + void clientConnected(WebSocketTransport* client); + +private Q_SLOTS: + void handleNewConnection(); + +private: + QWebSocketServer *m_server; +}; + +QT_END_NAMESPACE + +#endif // WEBSOCKETTRANSPORTSERVER_H diff --git a/libraries/ui/src/impl/websockettransport.cpp b/libraries/ui/src/impl/websockettransport.cpp new file mode 100644 index 0000000000..8ed330c72d --- /dev/null +++ b/libraries/ui/src/impl/websockettransport.cpp @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "websockettransport.h" + +#include +#include +#include + +#include + +/*! + \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. + + The transport delegates all messages received over the QWebSocket over its + textMessageReceived signal. Analogously, all calls to sendTextMessage will + be send over the QWebSocket to the remote client. +*/ + +QT_BEGIN_NAMESPACE + +/*! + Construct the transport object and wrap the given socket. + + The socket is also set as the parent of the transport object. +*/ +WebSocketTransport::WebSocketTransport(QWebSocket *socket) +: QWebChannelAbstractTransport(socket) +, m_socket(socket) +{ + connect(socket, &QWebSocket::textMessageReceived, + this, &WebSocketTransport::textMessageReceived); +} + +/*! + Destroys the WebSocketTransport. +*/ +WebSocketTransport::~WebSocketTransport() +{ + +} + +/*! + Serialize the JSON message and send it as a text message via the WebSocket to the client. +*/ +void WebSocketTransport::sendMessage(const QJsonObject &message) +{ + QJsonDocument doc(message); + m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); +} + +/*! + Deserialize the stringified JSON messageData and emit messageReceived. +*/ +void WebSocketTransport::textMessageReceived(const QString &messageData) +{ + QJsonParseError error; + QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); + if (error.error) { + qWarning() << "Failed to parse text message as JSON object:" << messageData + << "Error is:" << error.errorString(); + return; + } else if (!message.isObject()) { + qWarning() << "Received JSON message that is not an object: " << messageData; + return; + } + emit messageReceived(message.object(), this); +} + +QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websockettransport.h b/libraries/ui/src/impl/websockettransport.h new file mode 100644 index 0000000000..a1fdd3553a --- /dev/null +++ b/libraries/ui/src/impl/websockettransport.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBSOCKETTRANSPORT_H +#define WEBSOCKETTRANSPORT_H + +#include + +QT_BEGIN_NAMESPACE + +class QWebSocket; +class WebSocketTransport : public QWebChannelAbstractTransport +{ + Q_OBJECT +public: + explicit WebSocketTransport(QWebSocket *socket); + virtual ~WebSocketTransport(); + + void sendMessage(const QJsonObject &message) Q_DECL_OVERRIDE; + +private Q_SLOTS: + void textMessageReceived(const QString &message); + +private: + QWebSocket *m_socket; +}; + +QT_END_NAMESPACE + +#endif // WEBSOCKETTRANSPORT_H diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 8fda001e14..f94a0b85c0 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,15 +2,32 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Widgets OpenGL Network Qml Quick Script) +setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) + # Issue causes build failure unless we add this directory. + # See https://bugreports.qt.io/browse/QTBUG-43351 + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) +# copy the resources files beside the executable +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/qml" + $/qml +) + + +target_glew() + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") +endif() + package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 18f62dc016..75397ad95f 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,41 +1,92 @@ // -// main.cpp -// tests/render-utils/src -// -// Copyright 2014 High Fidelity, Inc. +// Created by Bradley Austin Davis on 2015-04-22 +// Copyright 2013-2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "OffscreenUi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include #include -#include -#include "MessageDialog.h" -#include "VrMenu.h" -#include "InfoView.h" -#include +#include +#include +#include +#include +#include +#include + +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getExamplesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getInterfaceQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} + +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} + class RateCounter { std::vector times; @@ -74,54 +125,302 @@ public: }; -class MenuConstants : public QObject{ - Q_OBJECT - Q_ENUMS(Item) -public: - enum Item { - RenderLookAtTargets, - }; -public: - MenuConstants(QObject* parent = nullptr) : QObject(parent) { +extern QOpenGLContext* qt_gl_global_share_context(); + +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qWarning() << qPrintable(message); + return true; } + return false; +} + +const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); + +static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { + QString message = ""; + for (int i = 0; i < context->argumentCount(); i++) { + if (i > 0) { + message += " "; + } + message += context->argument(i).toString(); + } + qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline + + message = message.replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("'", "\\'"); + engine->evaluate("Script.print('" + message + "')"); + + return QScriptValue(); +} + +class ScriptEngine : public QScriptEngine { + Q_OBJECT + +public: + void loadFile(const QString& scriptPath) { + if (_isRunning) { + return; + } + qDebug() << "Loading script from " << scriptPath; + _fileNameString = scriptPath; + + QFile file(scriptPath); + if (file.exists()) { + file.open(QIODevice::ReadOnly); + _scriptContents = file.readAll(); + } else { + qFatal("Missing file "); + } + runInThread(); + } + + Q_INVOKABLE void stop() { + if (!_isFinished) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } + _isFinished = true; + if (_wantSignals) { + emit runningStateChanged(); + } + } + } + + Q_INVOKABLE void print(const QString& message) { + if (_wantSignals) { + emit printedMessage(message); + } + } + + Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { + // create the timer, add it to the map, and start it + QTimer* newTimer = new QTimer(this); + newTimer->setSingleShot(isSingleShot); + + connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); + + // make sure the timer stops when the script does + connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); + + _timerFunctionMap.insert(newTimer, function); + + newTimer->start(intervalMS); + return newTimer; + } + + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { + return setupTimerWithInterval(function, intervalMS, false); + } + + Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { + return setupTimerWithInterval(function, timeoutMS, true); + } +private: + + void runInThread() { + QThread* workerThread = new QThread(); + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + connect(workerThread, &QThread::started, this, &ScriptEngine::run); + connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + moveToThread(workerThread); + workerThread->start(); + } + + void init() { + _isInitialized = true; + registerMetaTypes(this); + registerGlobalObject("Script", this); + qScriptRegisterSequenceMetaType>(this); + qScriptRegisterSequenceMetaType>(this); + globalObject().setProperty("QmlWebWindow", newFunction(QmlWebWindowClass::constructor)); + QScriptValue printConstructorValue = newFunction(debugPrint); + globalObject().setProperty("print", printConstructorValue); + } + + void timerFired() { + QTimer* callingTimer = reinterpret_cast(sender()); + QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); + + if (!callingTimer->isActive()) { + // this timer is done, we can kill it + _timerFunctionMap.remove(callingTimer); + delete callingTimer; + } + + // call the associated JS function, if it exists + if (timerFunction.isValid()) { + timerFunction.call(); + } + } + + + void run() { + if (!_isInitialized) { + init(); + } + + _isRunning = true; + if (_wantSignals) { + emit runningStateChanged(); + } + + QScriptValue result = evaluate(_scriptContents, _fileNameString); + QElapsedTimer startTime; + startTime.start(); + + int thisFrame = 0; + + qint64 lastUpdate = usecTimestampNow(); + + while (!_isFinished) { + int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec + if (usecToSleep > 0) { + usleep(usecToSleep); + } + + if (_isFinished) { + break; + } + + QCoreApplication::processEvents(); + if (_isFinished) { + break; + } + + qint64 now = usecTimestampNow(); + float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; + if (!_isFinished) { + if (_wantSignals) { + emit update(deltaTime); + } + } + lastUpdate = now; + + // Debug and clear exceptions + hadUncaughtExceptions(*this, _fileNameString); + } + + if (_wantSignals) { + emit scriptEnding(); + } + + if (_wantSignals) { + emit finished(_fileNameString, this); + } + + _isRunning = false; + + if (_wantSignals) { + emit runningStateChanged(); + emit doneRunning(); + } + } + + void registerGlobalObject(const QString& name, QObject* object) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + if (!globalObject().property(name).isValid()) { + if (object) { + QScriptValue value = newQObject(object); + globalObject().setProperty(name, value); + } else { + globalObject().setProperty(name, QScriptValue()); + } + } + } + + void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + globalObject().setProperty(name, scriptFun); + } + + void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + object.setProperty(name, scriptFun); + } + } + +signals: + void scriptLoaded(const QString& scriptFilename); + void errorLoadingScript(const QString& scriptFilename); + void update(float deltaTime); + void scriptEnding(); + void finished(const QString& fileNameString, ScriptEngine* engine); + void cleanupMenuItem(const QString& menuItemString); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void runningStateChanged(); + void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName, bool isUserLoaded); + void reloadScript(const QString& scriptName, bool isUserLoaded); + void doneRunning(); + + +private: + QString _scriptContents; + QString _fileNameString; + QString _parentURL; + bool _isInitialized { false }; + std::atomic _isFinished { false }; + std::atomic _isRunning { false }; + bool _wantSignals { true }; + bool _isThreaded { false }; + QHash _timerFunctionMap; }; -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; + + +ScriptEngine* loadScript(const QString& scriptFilename) { + ScriptEngine* scriptEngine = new ScriptEngine(); + scriptEngine->loadFile(scriptFilename); + return scriptEngine; } -const QString& getQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow, private QOpenGLFunctions { +class QTestWindow : public QWindow { Q_OBJECT QOpenGLContext* _context{ nullptr }; @@ -130,86 +429,103 @@ class QTestWindow : public QWindow, private QOpenGLFunctions { RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; + ProgramPtr _program; + ShapeWrapperPtr _plane; + QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { + _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - connect(&_timer, &QTimer::timeout, [=] { - draw(); - }); + QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); - DependencyManager::set(); - setSurfaceType(QSurface::OpenGLSurface); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - QSurfaceFormat format; - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); - format.setOption(QSurfaceFormat::DebugContext); + { + setSurfaceType(QSurface::OpenGLSurface); + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + setFormat(format); + _context = new QOpenGLContext; + _context->setFormat(format); + _context->setShareContext(_chromiumShareContext->getContext()); + } - setFormat(format); - _context = new QOpenGLContext; - _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); - initializeOpenGLFunctions(); + { + qDebug() << (const char*)glGetString(GL_VERSION); QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); logger->initialize(); // initializes in the current context, i.e. ctx logger->enableMessages(); connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { qDebug() << debugMessage; }); - // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + //logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); } - qDebug() << (const char*)this->glGetString(GL_VERSION); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glDisable(GL_DEPTH_TEST); + glewExperimental = true; + glewInit(); + glGetError(); + + using namespace oglplus; + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); + Context::Disable(Capability::DepthTest); + Context::Disable(Capability::CullFace); + Context::ClearColor(0.2f, 0.2f, 0.2f, 1); MessageDialog::registerType(); - VrMenu::registerType(); InfoView::registerType(); + auto offscreenUi = DependencyManager::set(); + { + offscreenUi->create(_context); + offscreenUi->setProxyWindow(this); - auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_context); - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); - makeCurrent(); + makeCurrent(); + } - offscreenUi->setProxyWindow(this); - QDesktopWidget* desktop = QApplication::desktop(); - QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); - int height = rect.height(); - //rect.setHeight(height / 2); - rect.setY(rect.y() + height / 2); + + auto primaryScreen = QGuiApplication::primaryScreen(); + auto targetScreen = primaryScreen; + auto screens = QGuiApplication::screens(); + if (screens.size() > 1) { + for (auto screen : screens) { + if (screen != targetScreen) { + targetScreen = screen; + break; + } + } + } + auto rect = targetScreen->availableGeometry(); + rect.setWidth(rect.width() * 0.8f); + rect.setHeight(rect.height() * 0.8f); + rect.moveTo(QPoint(20, 20)); setGeometry(rect); -// setFramePosition(QPoint(-1000, 0)); -// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); - offscreenUi->load(QUrl("TestMenu.qml")); - // Requires a root menu to have been loaded before it can load - VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -227,16 +543,35 @@ private: } makeCurrent(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); + auto error = glGetError(); + if (error != GL_NO_ERROR) { + qDebug() << "GL error in entering draw " << error; + } - renderQml(); + using namespace oglplus; + Context::Clear().ColorBuffer().DepthBuffer(); + ivec2 size(_size.width(), _size.height()); + size *= devicePixelRatio(); + size = glm::max(size, ivec2(100, 100)); + Context::Viewport(size.x, size.y); + if (!_program) { + _program = loadDefaultShader(); + _plane = loadPlane(_program); + } + if (testQmlTexture > 0) { + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + _program->Bind(); + _plane->Use(); + _plane->Draw(); _context->swapBuffers(this); - glFinish(); fps.increment(); - if (fps.elapsed() >= 2.0f) { + if (fps.elapsed() >= 10.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -246,8 +581,6 @@ private: _context->makeCurrent(this); } - void renderQml(); - void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -269,11 +602,13 @@ protected: offscreenUi->load("Browser.qml"); } break; - case Qt::Key_L: + + case Qt::Key_J: if (event->modifiers() & Qt::CTRL) { - InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); + loadScript(getExamplesDir() + "tests/qmlWebTest.js"); } break; + case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -281,22 +616,9 @@ protected: }); } break; - case Qt::Key_J: - if (event->modifiers() & Qt::CTRL) { - auto offscreenUi = DependencyManager::get(); - rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); - QMetaObject::invokeMethod(rootMenu, "popup"); - } - break; } QWindow::keyPressEvent(event); } - QQmlContext* menuContext{ nullptr }; - void keyReleaseEvent(QKeyEvent *event) override { - if (_altPressed && Qt::Key_Alt == event->key()) { - VrMenu::toggle(); - } - } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -308,40 +630,26 @@ protected: } }; -void QTestWindow::renderQml() { - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - if (testQmlTexture > 0) { - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glBegin(GL_QUADS); - { - glTexCoord2f(0, 0); - glVertex2f(-1, -1); - glTexCoord2f(0, 1); - glVertex2f(-1, 1); - glTexCoord2f(1, 1); - glVertex2f(1, 1); - glTexCoord2f(1, 0); - glVertex2f(1, -1); - } - glEnd(); -} - - const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = message; + +#ifdef Q_OS_WIN + if (!logMessage.isEmpty()) { + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); + } +#endif +} + + int main(int argc, char** argv) { - QApplication app(argc, argv); + QGuiApplication app(argc, argv); + qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From 3b1ba67a889cf44a5349b955bf1d2e95072d8603 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 16 Dec 2015 17:08:22 -0800 Subject: [PATCH 11/41] ModelEntityItem: Fix for incorrect animations with preTranslations ModelEntities that were playing animations on models with local pivot offsets were not working correctly. Specifically, the windmill animation in the demo domain. We now compose a matrix containing all of the FBX's preTranslation, preRotation and postTranformations. --- libraries/animation/src/AnimSkeleton.cpp | 32 ++++++++++++---------- libraries/entities/src/ModelEntityItem.cpp | 20 ++++++++++---- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index c9c6e43a02..8e3d716aac 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -140,6 +140,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } #ifndef NDEBUG +#define DUMP_FBX_JOINTS void AnimSkeleton::dump() const { qCDebug(animation) << "["; for (int i = 0; i < getNumJoints(); i++) { @@ -151,21 +152,22 @@ void AnimSkeleton::dump() const { qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); #ifdef DUMP_FBX_JOINTS - qCDebug(animation) << " isFree =" << _joints[i].isFree; - qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; - qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; - qCDebug(animation) << " translation =" << _joints[i].translation; - qCDebug(animation) << " preTransform =" << _joints[i].preTransform; - qCDebug(animation) << " preRotation =" << _joints[i].preRotation; - qCDebug(animation) << " rotation =" << _joints[i].rotation; - qCDebug(animation) << " postRotation =" << _joints[i].postRotation; - qCDebug(animation) << " postTransform =" << _joints[i].postTransform; - qCDebug(animation) << " transform =" << _joints[i].transform; - qCDebug(animation) << " rotationMin =" << _joints[i].rotationMin << ", rotationMax =" << _joints[i].rotationMax; - qCDebug(animation) << " inverseDefaultRotation" << _joints[i].inverseDefaultRotation; - qCDebug(animation) << " inverseBindRotation" << _joints[i].inverseBindRotation; - qCDebug(animation) << " bindTransform" << _joints[i].bindTransform; - qCDebug(animation) << " isSkeletonJoint" << _joints[i].isSkeletonJoint; + qCDebug(animation) << " fbxJoint ="; + qCDebug(animation) << " isFree =" << _joints[i].isFree; + qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; + qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; + qCDebug(animation) << " translation =" << _joints[i].translation; + qCDebug(animation) << " preTransform =" << _joints[i].preTransform; + qCDebug(animation) << " preRotation =" << _joints[i].preRotation; + qCDebug(animation) << " rotation =" << _joints[i].rotation; + qCDebug(animation) << " postRotation =" << _joints[i].postRotation; + qCDebug(animation) << " postTransform =" << _joints[i].postTransform; + qCDebug(animation) << " transform =" << _joints[i].transform; + qCDebug(animation) << " rotationMin =" << _joints[i].rotationMin << ", rotationMax =" << _joints[i].rotationMax; + qCDebug(animation) << " inverseDefaultRotation" << _joints[i].inverseDefaultRotation; + qCDebug(animation) << " inverseBindRotation" << _joints[i].inverseBindRotation; + qCDebug(animation) << " bindTransform" << _joints[i].bindTransform; + qCDebug(animation) << " isSkeletonJoint" << _joints[i].isSkeletonJoint; #endif if (getParentIndex(i) >= 0) { qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 352ab4d701..a1f16ee251 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "EntitiesLogging.h" #include "EntityItemProperties.h" @@ -243,13 +244,22 @@ void ModelEntityItem::getAnimationFrame(bool& newFrame, _lastKnownFrameDataRotations.resize(_jointMapping.size()); _lastKnownFrameDataTranslations.resize(_jointMapping.size()); + for (int j = 0; j < _jointMapping.size(); j++) { int index = _jointMapping[j]; - if (index != -1 && index < rotations.size()) { - _lastKnownFrameDataRotations[j] = fbxJoints[index].preRotation * rotations[index]; - } - if (index != -1 && index < translations.size()) { - _lastKnownFrameDataTranslations[j] = translations[index]; + if (index >= 0) { + glm::mat4 translationMat; + if (index < translations.size()) { + translationMat = glm::translate(translations[index]); + } + glm::mat4 rotationMat; + if (index < rotations.size()) { + rotationMat = glm::mat4_cast(rotations[index]); + } + glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * + rotationMat * fbxJoints[index].postTransform); + _lastKnownFrameDataTranslations[j] = extractTranslation(finalMat); + _lastKnownFrameDataRotations[j] = glmExtractRotation(finalMat); } } } From 0b9bd858a7c1fedecca7b32190155b5038ba6491 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 16 Dec 2015 21:28:27 -0800 Subject: [PATCH 12/41] implement support for pose ScriptEndpoints --- examples/controllers/reticleTests.js | 42 +++++++++++++++++ .../controllers/src/controllers/Pose.cpp | 18 +++++++- .../impl/endpoints/ScriptEndpoint.cpp | 46 ++++++++++++++++++- .../impl/endpoints/ScriptEndpoint.h | 13 ++++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/examples/controllers/reticleTests.js b/examples/controllers/reticleTests.js index 4c805c8b1c..3392d1b7d9 100644 --- a/examples/controllers/reticleTests.js +++ b/examples/controllers/reticleTests.js @@ -34,6 +34,48 @@ var mappingJSON = { mapping = Controller.parseMapping(JSON.stringify(mappingJSON)); mapping.enable(); + +var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; +var mapping2 = Controller.newMapping(MAPPING_NAME); +//mapping2.from(Controller.Standard.RightHand).debug(true).to(Controller.Actions.LeftHand); + +/* +mapping2.from(Controller.Standard.RightHand).peek().to(function(pose) { + print("Controller.Standard.RightHand value:" + JSON.stringify(pose)); + }); +*/ + +var translation = { x: 0, y: 0.1, z: 0 }; +var translationDx = 0.01; +var translationDy = 0.01; +var translationDz = 0.01; +var TRANSLATION_LIMIT = 0.5; +var rotation = Quat.fromPitchYawRollDegrees(45, 0, 45); +mapping2.from(function() { + translation.x = translation.x + translationDx; + translation.y = translation.y + translationDy; + translation.z = translation.z + translationDz; + if ((translation.x > TRANSLATION_LIMIT) || (translation.x < (-1 *TRANSLATION_LIMIT))) { + translationDx = translationDx * -1; + } + if ((translation.y > TRANSLATION_LIMIT) || (translation.y < (-1 *TRANSLATION_LIMIT))) { + translationDy = translationDy * -1; + } + if ((translation.z > TRANSLATION_LIMIT) || (translation.z < (-1 *TRANSLATION_LIMIT))) { + translationDz = translationDz * -1; + } + var pose = { + translation: translation, + rotation: rotation, + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0, w: 1 } + }; + return pose; +}).debug(true).to(Controller.Standard.LeftHand); + +Controller.enableMapping(MAPPING_NAME); + + Script.scriptEnding.connect(function(){ mapping.disable(); }); diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index 2281fc98ff..2e1d30cff2 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -42,7 +42,23 @@ namespace controller { } void Pose::fromScriptValue(const QScriptValue& object, Pose& pose) { - // nothing for now... + bool isValid = true; + auto translation = object.property("translation"); + auto rotation = object.property("rotation"); + auto velocity = object.property("velocity"); + auto angularVelocity = object.property("angularVelocity"); + if (translation.isValid() && + rotation.isValid() && + velocity.isValid() && + angularVelocity.isValid()) { + vec3FromScriptValue(translation, pose.translation); + quatFromScriptValue(rotation, pose.rotation); + vec3FromScriptValue(velocity, pose.velocity); + quatFromScriptValue(angularVelocity, pose.angularVelocity); + pose.valid = true; + } else { + pose.valid = false; + } } } diff --git a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp index bb9517b136..3e7fde347e 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp @@ -10,6 +10,8 @@ #include +#include + using namespace controller; float ScriptEndpoint::peek() const { @@ -23,7 +25,16 @@ void ScriptEndpoint::updateValue() { return; } - _lastValueRead = (float)_callable.call().toNumber(); + QScriptValue result = _callable.call(); + + // If the callable ever returns a non-number, we assume it's a pose + // and start reporting ourselves as a pose. + if (result.isNumber()) { + _lastValueRead = (float)_callable.call().toNumber(); + } else { + Pose::fromScriptValue(result, _lastPoseRead); + _returnPose = true; + } } void ScriptEndpoint::apply(float value, const Pointer& source) { @@ -44,3 +55,36 @@ void ScriptEndpoint::internalApply(float value, int sourceID) { _callable.call(QScriptValue(), QScriptValueList({ QScriptValue(value), QScriptValue(sourceID) })); } + +Pose ScriptEndpoint::peekPose() const { + const_cast(this)->updatePose(); + return _lastPoseRead; +} + +void ScriptEndpoint::updatePose() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "updatePose", Qt::QueuedConnection); + return; + } + QScriptValue result = _callable.call(); + Pose::fromScriptValue(result, _lastPoseRead); +} + +void ScriptEndpoint::apply(const Pose& newPose, const Pointer& source) { + if (newPose == _lastPoseWritten) { + return; + } + internalApply(newPose, source->getInput().getID()); +} + +void ScriptEndpoint::internalApply(const Pose& newPose, int sourceID) { + _lastPoseWritten = newPose; + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "internalApply", Qt::QueuedConnection, + Q_ARG(const Pose&, newPose), + Q_ARG(int, sourceID)); + return; + } + _callable.call(QScriptValue(), + QScriptValueList({ Pose::toScriptValue(_callable.engine(), newPose), QScriptValue(sourceID) })); +} diff --git a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h index 836af721f6..00439f5560 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h @@ -27,13 +27,26 @@ public: virtual float peek() const override; virtual void apply(float newValue, const Pointer& source) override; + + virtual Pose peekPose() const override; + virtual void apply(const Pose& newValue, const Pointer& source) override; + + virtual bool isPose() const override { return _returnPose; } + protected: Q_INVOKABLE void updateValue(); Q_INVOKABLE virtual void internalApply(float newValue, int sourceID); + + Q_INVOKABLE void updatePose(); + Q_INVOKABLE virtual void internalApply(const Pose& newValue, int sourceID); private: QScriptValue _callable; float _lastValueRead { 0.0f }; float _lastValueWritten { 0.0f }; + + bool _returnPose { false }; + Pose _lastPoseRead; + Pose _lastPoseWritten; }; } From 63a63152a48afe98be231bfd653b3c17c3f7cc02 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Dec 2015 11:10:49 -0800 Subject: [PATCH 13/41] add procedural hand pose example --- .../controllers/proceduralHandPoseExample.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 examples/controllers/proceduralHandPoseExample.js diff --git a/examples/controllers/proceduralHandPoseExample.js b/examples/controllers/proceduralHandPoseExample.js new file mode 100644 index 0000000000..5d0b10c46a --- /dev/null +++ b/examples/controllers/proceduralHandPoseExample.js @@ -0,0 +1,76 @@ +// +// proceduralHandPoseExample.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var MAPPING_NAME = "com.highfidelity.examples.proceduralHandPose"; +var mapping = Controller.newMapping(MAPPING_NAME); +var translation = { x: 0, y: 0.1, z: 0 }; +var translationDx = 0.01; +var translationDy = 0.01; +var translationDz = -0.01; +var TRANSLATION_LIMIT = 0.5; + +var pitch = 45; +var yaw = 0; +var roll = 45; +var pitchDelta = 1; +var yawDelta = -1; +var rollDelta = 1; +var ROTATION_MIN = -90; +var ROTATION_MAX = 90; + +mapping.from(function() { + + // adjust the hand translation in a periodic back and forth motion for each of the 3 axes + translation.x = translation.x + translationDx; + translation.y = translation.y + translationDy; + translation.z = translation.z + translationDz; + if ((translation.x > TRANSLATION_LIMIT) || (translation.x < (-1 * TRANSLATION_LIMIT))) { + translationDx = translationDx * -1; + } + if ((translation.y > TRANSLATION_LIMIT) || (translation.y < (-1 * TRANSLATION_LIMIT))) { + translationDy = translationDy * -1; + } + if ((translation.z > TRANSLATION_LIMIT) || (translation.z < (-1 * TRANSLATION_LIMIT))) { + translationDz = translationDz * -1; + } + + // adjust the hand rotation in a periodic back and forth motion for each of pitch/yaw/roll + pitch = pitch + pitchDelta; + yaw = yaw + yawDelta; + roll = roll + rollDelta; + if ((pitch > ROTATION_MAX) || (pitch < ROTATION_MIN)) { + pitchDelta = pitchDelta * -1; + } + if ((yaw > ROTATION_MAX) || (yaw < ROTATION_MIN)) { + yawDelta = yawDelta * -1; + } + if ((roll > ROTATION_MAX) || (roll < ROTATION_MIN)) { + rollDelta = rollDelta * -1; + } + + var rotation = Quat.fromPitchYawRollDegrees(pitch, yaw, roll); + + var pose = { + translation: translation, + rotation: rotation, + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0, w: 1 } + }; + return pose; +}).debug(true).to(Controller.Standard.LeftHand); + +Controller.enableMapping(MAPPING_NAME); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); From f3adb8a2f70264af4eb50b93b314065ce88d059b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 17 Dec 2015 11:16:59 -0800 Subject: [PATCH 14/41] fix offset math in hold action --- interface/src/avatar/AvatarActionHold.cpp | 55 +++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 34b959575d..b2343c8bce 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -48,38 +48,38 @@ void AvatarActionHold::prepareForPhysicsSimulation() { auto avatarManager = DependencyManager::get(); auto holdingAvatar = std::static_pointer_cast(avatarManager->getAvatarBySessionID(_holderID)); - if (!holdingAvatar) { + if (!holdingAvatar || !holdingAvatar->isMyAvatar()) { return; } withWriteLock([&]{ - if (_ignoreIK && holdingAvatar->isMyAvatar()) { + if (_ignoreIK) { return; } - if (holdingAvatar->isMyAvatar()) { - glm::vec3 palmPosition; - glm::quat palmRotation; - if (_hand == "right") { - palmPosition = holdingAvatar->getRightPalmPosition(); - palmRotation = holdingAvatar->getRightPalmRotation(); - } else { - palmPosition = holdingAvatar->getLeftPalmPosition(); - palmRotation = holdingAvatar->getLeftPalmRotation(); - } - glm::vec3 avatarRigidBodyPosition; - glm::quat avatarRigidBodyRotation; - getAvatarRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); - - // determine the difference in translation and rotation between the avatar's - // rigid body and the palm position. The avatar's rigid body will be moved by bullet - // between this call and the call to getTarget, below. A call to get*PalmPosition in - // getTarget would get the palm position of the previous location of the avatar (because - // bullet has moved the av's rigid body but the rigid body's location has not yet been - // copied out into the Avatar class. - _palmOffsetFromRigidBody = palmPosition - avatarRigidBodyPosition; - _palmRotationFromRigidBody = glm::inverse(avatarRigidBodyRotation) * palmRotation; + glm::vec3 palmPosition; + glm::quat palmRotation; + if (_hand == "right") { + palmPosition = holdingAvatar->getRightPalmPosition(); + palmRotation = holdingAvatar->getRightPalmRotation(); + } else { + palmPosition = holdingAvatar->getLeftPalmPosition(); + palmRotation = holdingAvatar->getLeftPalmRotation(); } + + glm::vec3 avatarRigidBodyPosition; + glm::quat avatarRigidBodyRotation; + getAvatarRigidBodyLocation(avatarRigidBodyPosition, avatarRigidBodyRotation); + + // determine the difference in translation and rotation between the avatar's + // rigid body and the palm position. The avatar's rigid body will be moved by bullet + // between this call and the call to getTarget, below. A call to get*PalmPosition in + // getTarget would get the palm position of the previous location of the avatar (because + // bullet has moved the av's rigid body but the rigid body's location has not yet been + // copied out into the Avatar class. + glm::quat avatarRotationInverse = glm::inverse(avatarRigidBodyRotation); + _palmOffsetFromRigidBody = avatarRotationInverse * (palmPosition - avatarRigidBodyPosition); + _palmRotationFromRigidBody = avatarRotationInverse * palmRotation; }); } @@ -116,13 +116,8 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve // and the data in the Avatar class is stale. This means that the result of get*PalmPosition will // be stale. Instead, determine the current palm position with the current avatar's rigid body // location and the saved offsets. - palmPosition = avatarRigidBodyPosition + _palmOffsetFromRigidBody; + palmPosition = avatarRigidBodyPosition + avatarRigidBodyRotation * _palmOffsetFromRigidBody; palmRotation = avatarRigidBodyRotation * _palmRotationFromRigidBody; - if (isRightHand) { - palmRotation = holdingAvatar->getRightPalmRotation(); - } else { - palmRotation = holdingAvatar->getLeftPalmRotation(); - } } else { if (isRightHand) { palmPosition = holdingAvatar->getRightPalmPosition(); From b80fa1c8064c23bb3709593dafcef968cb7e1aa3 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 17 Dec 2015 11:46:41 -0800 Subject: [PATCH 15/41] code review --- interface/src/avatar/AvatarActionHold.cpp | 29 +++++++++++++++++++---- interface/src/avatar/AvatarActionHold.h | 3 ++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index b2343c8bce..be562e2773 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -78,8 +78,16 @@ void AvatarActionHold::prepareForPhysicsSimulation() { // bullet has moved the av's rigid body but the rigid body's location has not yet been // copied out into the Avatar class. glm::quat avatarRotationInverse = glm::inverse(avatarRigidBodyRotation); - _palmOffsetFromRigidBody = avatarRotationInverse * (palmPosition - avatarRigidBodyPosition); - _palmRotationFromRigidBody = avatarRotationInverse * palmRotation; + + // the offset should be in the frame of the avatar, but something about the order + // things are updated makes this wrong: + // _palmOffsetFromRigidBody = avatarRotationInverse * (palmPosition - avatarRigidBodyPosition); + // I'll leave it here as a comment in case avatar handling changes. + _palmOffsetFromRigidBody = palmPosition - avatarRigidBodyPosition; + + // rotation should also be needed, but again, the order of updates makes this unneeded. leaving + // code here for future reference. + // _palmRotationFromRigidBody = avatarRotationInverse * palmRotation; }); } @@ -116,8 +124,21 @@ std::shared_ptr AvatarActionHold::getTarget(glm::quat& rotation, glm::ve // and the data in the Avatar class is stale. This means that the result of get*PalmPosition will // be stale. Instead, determine the current palm position with the current avatar's rigid body // location and the saved offsets. - palmPosition = avatarRigidBodyPosition + avatarRigidBodyRotation * _palmOffsetFromRigidBody; - palmRotation = avatarRigidBodyRotation * _palmRotationFromRigidBody; + + // this line is more correct but breaks for the current way avatar data is updated. + // palmPosition = avatarRigidBodyPosition + avatarRigidBodyRotation * _palmOffsetFromRigidBody; + // instead, use this for now: + palmPosition = avatarRigidBodyPosition + _palmOffsetFromRigidBody; + + // the item jitters the least by getting the rotation based on the opinion of Avatar.h rather + // than that of the rigid body. leaving this next line here for future reference: + // palmRotation = avatarRigidBodyRotation * _palmRotationFromRigidBody; + + if (isRightHand) { + palmRotation = holdingAvatar->getRightPalmRotation(); + } else { + palmRotation = holdingAvatar->getLeftPalmRotation(); + } } else { if (isRightHand) { palmPosition = holdingAvatar->getRightPalmPosition(); diff --git a/interface/src/avatar/AvatarActionHold.h b/interface/src/avatar/AvatarActionHold.h index fc8baf6dcc..b97ec59780 100644 --- a/interface/src/avatar/AvatarActionHold.h +++ b/interface/src/avatar/AvatarActionHold.h @@ -61,7 +61,8 @@ private: glm::vec3 _previousPositionalDelta; glm::vec3 _palmOffsetFromRigidBody; - glm::quat _palmRotationFromRigidBody; + // leaving this here for future refernece. + // glm::quat _palmRotationFromRigidBody; }; #endif // hifi_AvatarActionHold_h From 82c865af2c79be998c3d9c72abb6cfd87d3a9cdd Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 17 Dec 2015 12:20:38 -0800 Subject: [PATCH 16/41] Rename QmlWebWindow to OverlayWebWindow, move ctor registration to script engine --- examples/tests/qmlWebTest.js | 2 +- interface/src/Application.cpp | 1 - libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 +++ tests/ui/src/main.cpp | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js index 250852abe7..f905e494dc 100644 --- a/examples/tests/qmlWebTest.js +++ b/examples/tests/qmlWebTest.js @@ -1,6 +1,6 @@ print("Launching web window"); -webWindow = new QmlWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); print("JS Side window: " + webWindow); print("JS Side bridge: " + webWindow.eventBridge); webWindow.eventBridge.webEventReceived.connect(function(data) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f0d41f0426..8bff85688e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4155,7 +4155,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); - scriptEngine->registerFunction("QmlWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 3796abd92a..c1131765f7 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ded3db11e9..5f39bee9fa 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -350,6 +351,8 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); + + registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 75397ad95f..00d1f2dc00 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -250,7 +250,7 @@ private: registerGlobalObject("Script", this); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); - globalObject().setProperty("QmlWebWindow", newFunction(QmlWebWindowClass::constructor)); + globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); QScriptValue printConstructorValue = newFunction(debugPrint); globalObject().setProperty("print", printConstructorValue); } From 5088eec2a2a2bd67f442083c285dd90573b2818e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 17 Dec 2015 12:23:39 -0800 Subject: [PATCH 17/41] Move directory web UI into the overlay --- examples/directory.js | 2 +- interface/resources/qml/QmlWebWindow.qml | 20 +++++++++++++++++++- libraries/ui/src/QmlWebWindowClass.cpp | 7 +++++++ libraries/ui/src/QmlWebWindowClass.h | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/examples/directory.js b/examples/directory.js index b1fac19e8b..d2a3768051 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index d59ef4955e..9664c0edb8 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -19,12 +19,27 @@ VrDialog { backgroundColor: "#7f000000" property url source: "about:blank" + signal navigating(string url) + Component.onCompleted: { enabled = true console.log("Web Window Created " + root); webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); + + webview.loadingChanged.connect(handleWebviewLoading) + } + + + function handleWebviewLoading(loadRequest) { + var HIFI_URL_PATTERN = /^hifi:\/\//; + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var newUrl = loadRequest.url.toString(); + if (newUrl.match(HIFI_URL_PATTERN)) { + root.navigating(newUrl); + } + } } Item { @@ -35,11 +50,14 @@ VrDialog { y: root.clientY width: root.clientWidth height: root.clientHeight - + WebEngineView { id: webview url: root.source anchors.fill: parent + profile: WebEngineProfile { + httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" + } } } // item } // dialog diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 3797034e79..2b7270edf0 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "impl/websocketclientwrapper.h" @@ -86,8 +87,14 @@ QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); Q_ASSERT(dynamic_cast(_qmlWindow)); + QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); } +void QmlWebWindowClass::handleNavigation(const QString& url) { + DependencyManager::get()->handleLookupString(url); +} + + void QmlWebWindowClass::setVisible(bool visible) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index c166fd2c20..1575ec7916 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -81,6 +81,7 @@ signals: private slots: void hasClosed(); + void handleNavigation(const QString& url); private: QmlScriptEventBridge* _eventBridge; From 762aefbbca611dd51a6c14b7d310738ff50cf8a6 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Dec 2015 17:38:20 -0800 Subject: [PATCH 18/41] add hand driven reticle --- examples/controllers/reticleHandTest.js | 35 +++++++++++++++ examples/controllers/reticleTests.js | 43 ------------------- .../src/controllers/ScriptingInterface.h | 3 ++ 3 files changed, 38 insertions(+), 43 deletions(-) create mode 100644 examples/controllers/reticleHandTest.js diff --git a/examples/controllers/reticleHandTest.js b/examples/controllers/reticleHandTest.js new file mode 100644 index 0000000000..5d0e2f238a --- /dev/null +++ b/examples/controllers/reticleHandTest.js @@ -0,0 +1,35 @@ +// +// reticleTest.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function moveReticle(pitch, yaw) { + //print("pitch:" + pitch); + //print("yaw:" + yaw); + + var globalPos = Controller.getReticlePosition(); + globalPos.x += yaw; + globalPos.y += pitch; + Controller.setReticlePosition(globalPos); +} + +var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from(Controller.Standard.RightHand).peek().to(function(pose) { + var angularVelocityEADs = Quat.safeEulerAngles(pose.angularVelocity); // degrees + var yaw = isNaN(angularVelocityEADs.y) ? 0 : (-angularVelocityEADs.y / 10); + var pitch = isNaN(angularVelocityEADs.x) ? 0 : (-angularVelocityEADs.x / 10); + moveReticle(pitch, yaw); +}); +mapping.enable(); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); diff --git a/examples/controllers/reticleTests.js b/examples/controllers/reticleTests.js index 3392d1b7d9..56b2ba413a 100644 --- a/examples/controllers/reticleTests.js +++ b/examples/controllers/reticleTests.js @@ -33,49 +33,6 @@ var mappingJSON = { mapping = Controller.parseMapping(JSON.stringify(mappingJSON)); mapping.enable(); - - -var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; -var mapping2 = Controller.newMapping(MAPPING_NAME); -//mapping2.from(Controller.Standard.RightHand).debug(true).to(Controller.Actions.LeftHand); - -/* -mapping2.from(Controller.Standard.RightHand).peek().to(function(pose) { - print("Controller.Standard.RightHand value:" + JSON.stringify(pose)); - }); -*/ - -var translation = { x: 0, y: 0.1, z: 0 }; -var translationDx = 0.01; -var translationDy = 0.01; -var translationDz = 0.01; -var TRANSLATION_LIMIT = 0.5; -var rotation = Quat.fromPitchYawRollDegrees(45, 0, 45); -mapping2.from(function() { - translation.x = translation.x + translationDx; - translation.y = translation.y + translationDy; - translation.z = translation.z + translationDz; - if ((translation.x > TRANSLATION_LIMIT) || (translation.x < (-1 *TRANSLATION_LIMIT))) { - translationDx = translationDx * -1; - } - if ((translation.y > TRANSLATION_LIMIT) || (translation.y < (-1 *TRANSLATION_LIMIT))) { - translationDy = translationDy * -1; - } - if ((translation.z > TRANSLATION_LIMIT) || (translation.z < (-1 *TRANSLATION_LIMIT))) { - translationDz = translationDz * -1; - } - var pose = { - translation: translation, - rotation: rotation, - velocity: { x: 0, y: 0, z: 0 }, - angularVelocity: { x: 0, y: 0, z: 0, w: 1 } - }; - return pose; -}).debug(true).to(Controller.Standard.LeftHand); - -Controller.enableMapping(MAPPING_NAME); - - Script.scriptEnding.connect(function(){ mapping.disable(); }); diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 9af478e709..9d3942cd55 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -87,6 +88,8 @@ namespace controller { Q_INVOKABLE QObject* parseMapping(const QString& json); Q_INVOKABLE QObject* loadMapping(const QString& jsonUrl); + Q_INVOKABLE glm::vec2 getReticlePosition() { return toGlm(QCursor::pos()); } + Q_INVOKABLE void setReticlePosition(glm::vec2 position) { QCursor::setPos(position.x, position.y); } //Q_INVOKABLE bool isPrimaryButtonPressed() const; //Q_INVOKABLE glm::vec2 getPrimaryJoystickPosition() const; From 5aef309b8a67fc456124a3f322bea42c44e05479 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:09:36 +1300 Subject: [PATCH 19/41] Get particles in particlesTest.js emitting again --- examples/example/entities/particlesTest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 5f95c348b2..10ff314702 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -255,7 +255,6 @@ textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png", color: { red: 255, green: 255, blue: 255 }, lifespan: 5.0, - visible: false, locked: false, isEmitting: false, lifetime: 3600 // 1 hour; just in case From d344ef6e2a98cc26da192ce81024f805265a683e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Dec 2015 18:10:41 -0800 Subject: [PATCH 20/41] fix warning in my PR --- libraries/controllers/src/controllers/Pose.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index 2e1d30cff2..8f43242431 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -42,7 +42,6 @@ namespace controller { } void Pose::fromScriptValue(const QScriptValue& object, Pose& pose) { - bool isValid = true; auto translation = object.property("translation"); auto rotation = object.property("rotation"); auto velocity = object.property("velocity"); From e2ecc388ca2efc69e900db2e155c063245c05e54 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:10:47 +1300 Subject: [PATCH 21/41] Indicate particle properties that (temporarily) aren't working --- examples/example/entities/particlesTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 10ff314702..2b1ab62791 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -60,7 +60,7 @@ }); break; case 3: - print("Radius spread"); + print("Radius spread - temporarily not working"); Entities.editEntity(particles, { accelerationSpread: { x: 0.0, y: 0.0, z: 0.0 }, radiusSpread: 0.035 @@ -83,7 +83,7 @@ }); break; case 6: - print("Alpha spread"); + print("Alpha spread - temporarily not working"); Entities.editEntity(particles, { alpha: 0.5, alphaSpread: 0.5 @@ -99,7 +99,7 @@ }); break; case 8: - print("Color spread"); + print("Color spread - temporarily not working"); Entities.editEntity(particles, { alpha: 1.0, alphaStart: 1.0, From c3e3af7c8c73c43c0a302804ba27ed6de4a35818 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:11:43 +1300 Subject: [PATCH 22/41] Document effect of Bezier interpolation on particle radius simulation --- examples/example/entities/particlesTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 2b1ab62791..e7f88ce468 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -71,6 +71,7 @@ Entities.editEntity(particles, { radiusSpread: 0.0, radiusStart: 0.0, + particleRadius: 2 * PARTICLE_RADIUS, // Bezier interpolation used means that middle value isn't intersected radiusFinish: 0.0 }); break; @@ -78,6 +79,7 @@ print("Alpha 0.5"); Entities.editEntity(particles, { radiusStart: PARTICLE_RADIUS, + particleRadius: PARTICLE_RADIUS, radiusFinish: PARTICLE_RADIUS, alpha: 0.5 }); From 9a7c0cb182aa527cfce75206889e38b283423988 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:12:03 +1300 Subject: [PATCH 23/41] Typo noticed in passing --- libraries/entities-renderer/src/textured_particle.slv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv index 1e9275ec72..e1f18c2b5f 100644 --- a/libraries/entities-renderer/src/textured_particle.slv +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -82,7 +82,7 @@ void main(void) { varColor = interpolate3Vec4(particle.color.start, particle.color.middle, particle.color.finish, age); // anchor point in eye space - float radius = bezierInterpolate(particle.radius.start, particle.radius.middle, particle.radius.finish , age); + float radius = bezierInterpolate(particle.radius.start, particle.radius.middle, particle.radius.finish, age); vec4 quadPos = radius * UNIT_QUAD[twoTriID]; vec4 anchorPoint; From 0b781a3589be3a3b604054f36e531714d7aff94a Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Dec 2015 18:39:05 -0800 Subject: [PATCH 24/41] fix warnings --- interface/src/avatar/Avatar.cpp | 2 +- interface/src/avatar/AvatarActionHold.cpp | 2 +- interface/src/ui/ApplicationCompositor.cpp | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f8040754d7..6094a0b9fe 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -157,7 +157,7 @@ void Avatar::animateScaleChanges(float deltaTime) { // snap to the end when we get close enough const float MIN_RELATIVE_SCALE_ERROR = 0.03f; - if (fabsf(_targetScale - currentScale) / _targetScale < 0.03f) { + if (fabsf(_targetScale - currentScale) / _targetScale < MIN_RELATIVE_SCALE_ERROR) { animatedScale = _targetScale; } diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index be562e2773..689d557c48 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -77,7 +77,7 @@ void AvatarActionHold::prepareForPhysicsSimulation() { // getTarget would get the palm position of the previous location of the avatar (because // bullet has moved the av's rigid body but the rigid body's location has not yet been // copied out into the Avatar class. - glm::quat avatarRotationInverse = glm::inverse(avatarRigidBodyRotation); + //glm::quat avatarRotationInverse = glm::inverse(avatarRigidBodyRotation); // the offset should be in the frame of the avatar, but something about the order // things are updated makes this wrong: diff --git a/interface/src/ui/ApplicationCompositor.cpp b/interface/src/ui/ApplicationCompositor.cpp index e5ecdfe217..abc3520c83 100644 --- a/interface/src/ui/ApplicationCompositor.cpp +++ b/interface/src/ui/ApplicationCompositor.cpp @@ -34,7 +34,6 @@ static const quint64 MSECS_TO_USECS = 1000ULL; static const quint64 TOOLTIP_DELAY = 500 * MSECS_TO_USECS; -static const float RETICLE_COLOR[] = { 0.0f, 198.0f / 255.0f, 244.0f / 255.0f }; static const float reticleSize = TWO_PI / 100.0f; static const float CURSOR_PIXEL_SIZE = 32.0f; From a7824a06d0b2d3bfa9d4fba2da1761d8e7bbf384 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 17 Dec 2015 18:51:27 -0800 Subject: [PATCH 25/41] fix Assignment copy constructor warning --- libraries/networking/src/Assignment.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 4c72a16f1d..e6163c776b 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -91,7 +91,7 @@ Assignment::Assignment(ReceivedMessage& message) : #endif -Assignment::Assignment(const Assignment& otherAssignment) { +Assignment::Assignment(const Assignment& otherAssignment) : QObject() { _uuid = otherAssignment._uuid; _command = otherAssignment._command; _type = otherAssignment._type; From d6541e9ee77ce1fdca7ea003b08f4030b3854b4e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Dec 2015 11:06:58 -0800 Subject: [PATCH 26/41] angular velocity is a vector, not a quaternion --- interface/src/avatar/MyAvatar.cpp | 14 ++++++------ libraries/avatars/src/HandData.cpp | 8 +++---- libraries/avatars/src/HandData.h | 19 ++++++++-------- .../controllers/src/controllers/Pose.cpp | 6 ++--- libraries/controllers/src/controllers/Pose.h | 6 ++--- plugins/hifiSixense/src/SixenseManager.cpp | 22 ++++++++----------- plugins/hifiSixense/src/SixenseManager.h | 8 +++---- 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5c8230bd88..c989f1dc71 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -509,25 +509,25 @@ glm::vec3 MyAvatar::getRightHandTipPosition() const { controller::Pose MyAvatar::getLeftHandPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::LeftHand); return palmData.isValid() ? controller::Pose(palmData.getPosition(), palmData.getRotation(), - palmData.getVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getRightHandPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::RightHand); return palmData.isValid() ? controller::Pose(palmData.getPosition(), palmData.getRotation(), - palmData.getVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getLeftHandTipPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::LeftHand); return palmData.isValid() ? controller::Pose(palmData.getTipPosition(), palmData.getRotation(), - palmData.getTipVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getTipVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getRightHandTipPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::RightHand); return palmData.isValid() ? controller::Pose(palmData.getTipPosition(), palmData.getRotation(), - palmData.getTipVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getTipVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } // virtual @@ -536,7 +536,7 @@ void MyAvatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (!_shouldRender) { return; // exit early } - + Avatar::render(renderArgs, cameraPosition); } @@ -799,7 +799,7 @@ void MyAvatar::updateLookAtTargetAvatar() { const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; AvatarHash hash = DependencyManager::get()->getHashCopy(); - + foreach (const AvatarSharedPointer& avatarPointer, hash) { auto avatar = static_pointer_cast(avatarPointer); bool isCurrentTarget = avatar->getIsLookAtTarget(); @@ -1175,7 +1175,7 @@ void MyAvatar::renderBody(RenderArgs* renderArgs, ViewFrustum* renderFrustum, fl if (!_skeletonModel.isRenderable()) { return; // wait until all models are loaded } - + fixupModelsInScene(); // Render head so long as the camera isn't inside it diff --git a/libraries/avatars/src/HandData.cpp b/libraries/avatars/src/HandData.cpp index 5d783a671e..7ba23b01ad 100644 --- a/libraries/avatars/src/HandData.cpp +++ b/libraries/avatars/src/HandData.cpp @@ -14,7 +14,7 @@ #include #include -#include "AvatarData.h" +#include "AvatarData.h" #include "HandData.h" @@ -38,7 +38,7 @@ PalmData& HandData::addNewPalm(Hand whichHand) { PalmData HandData::getCopyOfPalmData(Hand hand) const { QReadLocker locker(&_palmsLock); - // the palms are not necessarily added in left-right order, + // the palms are not necessarily added in left-right order, // so we have to search for the correct hand for (const auto& palm : _palms) { if (palm.whichHand() == hand && palm.isActive()) { @@ -64,7 +64,7 @@ void PalmData::addToPosition(const glm::vec3& delta) { _rawPosition += _owningHandData->worldToLocalVector(delta); } -bool HandData::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, +bool HandData::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, const PalmData*& collidingPalm) const { QReadLocker locker(&_palmsLock); @@ -93,7 +93,7 @@ glm::vec3 HandData::getBasePosition() const { float HandData::getBaseScale() const { return _owningAvatarData->getTargetScale(); } - + glm::vec3 PalmData::getFingerDirection() const { // finger points along yAxis in hand-frame const glm::vec3 LOCAL_FINGER_DIRECTION(0.0f, 1.0f, 0.0f); diff --git a/libraries/avatars/src/HandData.h b/libraries/avatars/src/HandData.h index d782f240ee..34ed610f80 100644 --- a/libraries/avatars/src/HandData.h +++ b/libraries/avatars/src/HandData.h @@ -38,7 +38,7 @@ public: HandData(AvatarData* owningAvatar); virtual ~HandData() {} - + // position conversion glm::vec3 localToWorldPosition(const glm::vec3& localPosition) { return getBasePosition() + getBaseOrientation() * localPosition * getBaseScale(); @@ -60,7 +60,7 @@ public: /// \param penetration[out] the vector in which to store the penetration /// \param collidingPalm[out] a const PalmData* to the palm that was collided with /// \return whether or not the sphere penetrated - bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, + bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, const PalmData*& collidingPalm) const; glm::quat getBaseOrientation() const; @@ -74,7 +74,7 @@ protected: AvatarData* _owningAvatarData; std::vector _palms; mutable QReadWriteLock _palmsLock{ QReadWriteLock::Recursive }; - + glm::vec3 getBasePosition() const; float getBaseScale() const; @@ -112,13 +112,12 @@ public: void setRawAngularVelocity(const glm::vec3& angularVelocity) { _rawAngularVelocity = angularVelocity; } const glm::vec3& getRawAngularVelocity() const { return _rawAngularVelocity; } - glm::quat getRawAngularVelocityAsQuat() const { return glm::quat(_rawAngularVelocity); } void addToPosition(const glm::vec3& delta); void addToPenetration(const glm::vec3& penetration) { _totalPenetration += penetration; } void resolvePenetrations() { addToPosition(-_totalPenetration); _totalPenetration = glm::vec3(0.0f); } - + void setTipPosition(const glm::vec3& position) { _tipPosition = position; } const glm::vec3 getTipPosition() const { return _owningHandData->localToWorldPosition(_tipPosition); } const glm::vec3& getTipRawPosition() const { return _tipPosition; } @@ -126,16 +125,16 @@ public: void setTipVelocity(const glm::vec3& velocity) { _tipVelocity = velocity; } const glm::vec3 getTipVelocity() const { return _owningHandData->localToWorldDirection(_tipVelocity); } const glm::vec3& getTipRawVelocity() const { return _tipVelocity; } - + void incrementFramesWithoutData() { _numFramesWithoutData++; } void resetFramesWithoutData() { _numFramesWithoutData = 0; } int getFramesWithoutData() const { return _numFramesWithoutData; } - + // FIXME - these are used in SkeletonModel::updateRig() the skeleton/rig should probably get this information // from an action and/or the UserInputMapper instead of piping it through here. void setTrigger(float trigger) { _trigger = trigger; } float getTrigger() const { return _trigger; } - + // return world-frame: glm::vec3 getFingerDirection() const; glm::vec3 getNormal() const; @@ -148,13 +147,13 @@ private: glm::vec3 _rawAngularVelocity; glm::quat _rawDeltaRotation; glm::quat _lastRotation; - + glm::vec3 _tipPosition; glm::vec3 _tipVelocity; glm::vec3 _totalPenetration; /// accumulator for per-frame penetrations float _trigger; - + bool _isActive; /// This has current valid data int _numFramesWithoutData; /// after too many frames without data, this tracked object assumed lost. HandData* _owningHandData; diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index 2281fc98ff..f07472e9e0 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -16,7 +16,7 @@ namespace controller { Pose::Pose(const vec3& translation, const quat& rotation, - const vec3& velocity, const quat& angularVelocity) : + const vec3& velocity, const vec3& angularVelocity) : translation(translation), rotation(rotation), velocity(velocity), angularVelocity(angularVelocity), valid (true) { } bool Pose::operator==(const Pose& right) const { @@ -26,7 +26,7 @@ namespace controller { } // FIXME add margin of error? Or add an additional withinEpsilon function? - return translation == right.getTranslation() && rotation == right.getRotation() && + return translation == right.getTranslation() && rotation == right.getRotation() && velocity == right.getVelocity() && angularVelocity == right.getAngularVelocity(); } @@ -35,7 +35,7 @@ namespace controller { obj.setProperty("translation", vec3toScriptValue(engine, pose.translation)); obj.setProperty("rotation", quatToScriptValue(engine, pose.rotation)); obj.setProperty("velocity", vec3toScriptValue(engine, pose.velocity)); - obj.setProperty("angularVelocity", quatToScriptValue(engine, pose.angularVelocity)); + obj.setProperty("angularVelocity", vec3toScriptValue(engine, pose.angularVelocity)); obj.setProperty("valid", pose.valid); return obj; diff --git a/libraries/controllers/src/controllers/Pose.h b/libraries/controllers/src/controllers/Pose.h index b8d27824f3..a8a4452758 100644 --- a/libraries/controllers/src/controllers/Pose.h +++ b/libraries/controllers/src/controllers/Pose.h @@ -23,12 +23,12 @@ namespace controller { vec3 translation; quat rotation; vec3 velocity; - quat angularVelocity; + vec3 angularVelocity; bool valid{ false }; Pose() {} Pose(const vec3& translation, const quat& rotation, - const vec3& velocity = vec3(), const quat& angularVelocity = quat()); + const vec3& velocity = vec3(), const vec3& angularVelocity = vec3()); Pose(const Pose&) = default; Pose& operator = (const Pose&) = default; @@ -38,7 +38,7 @@ namespace controller { vec3 getTranslation() const { return translation; } quat getRotation() const { return rotation; } vec3 getVelocity() const { return velocity; } - quat getAngularVelocity() const { return angularVelocity; } + vec3 getAngularVelocity() const { return angularVelocity; } static QScriptValue toScriptValue(QScriptEngine* engine, const Pose& event); static void fromScriptValue(const QScriptValue& object, Pose& event); diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 93329d768e..fb47697166 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -191,7 +191,7 @@ void SixenseManager::InputDevice::update(float deltaTime, bool jointsCaptured) { // Rotation of Palm glm::quat rotation(data->rot_quat[3], data->rot_quat[0], data->rot_quat[1], data->rot_quat[2]); handlePoseEvent(deltaTime, position, rotation, left); - rawPoses[i] = controller::Pose(position, rotation, glm::vec3(0), glm::quat()); + rawPoses[i] = controller::Pose(position, rotation, Vectors::ZERO, Vectors::ZERO); } else { _poseStateMap.clear(); _collectedSamples.clear(); @@ -457,25 +457,21 @@ void SixenseManager::InputDevice::handlePoseEvent(float deltaTime, glm::vec3 pos rotation = _avatarRotation * postOffset * glm::inverse(sixenseToHand) * rotation * preOffset * sixenseToHand; glm::vec3 velocity(0.0f); - glm::quat angularVelocity; + glm::vec3 angularVelocity(0.0f); if (prevPose.isValid() && deltaTime > std::numeric_limits::epsilon()) { + auto& samples = _collectedSamples[hand]; velocity = (position - prevPose.getTranslation()) / deltaTime; + samples.first.addSample(velocity); + velocity = samples.first.average; auto deltaRot = rotation * glm::conjugate(prevPose.getRotation()); auto axis = glm::axis(deltaRot); - auto angle = glm::angle(deltaRot); - angularVelocity = glm::angleAxis(angle / deltaTime, axis); - - // Average - auto& samples = _collectedSamples[hand]; - samples.first.addSample(velocity); - velocity = samples.first.average; - - // FIXME: // Not using quaternion average yet for angular velocity because it s probably wrong but keep the MovingAverage in place - //samples.second.addSample(glm::vec4(angularVelocity.x, angularVelocity.y, angularVelocity.z, angularVelocity.w)); - //angularVelocity = glm::quat(samples.second.average.w, samples.second.average.x, samples.second.average.y, samples.second.average.z); + auto speed = glm::angle(deltaRot) / deltaTime; + angularVelocity = speed * axis; + samples.second.addSample(angularVelocity); + angularVelocity = samples.second.average; } else if (!prevPose.isValid()) { _collectedSamples[hand].first.clear(); _collectedSamples[hand].second.clear(); diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index 5cd4dc2451..0de5fe93aa 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -49,12 +49,12 @@ private: static const int CALIBRATION_STATE_IDLE = 0; static const int CALIBRATION_STATE_IN_PROGRESS = 1; static const int CALIBRATION_STATE_COMPLETE = 2; - static const glm::vec3 DEFAULT_AVATAR_POSITION; + static const glm::vec3 DEFAULT_AVATAR_POSITION; static const float CONTROLLER_THRESHOLD; - + template using SampleAverage = MovingAverage; - using Samples = std::pair, SampleAverage>; + using Samples = std::pair, SampleAverage>; using MovingAverageMap = std::map; class InputDevice : public controller::InputDevice { @@ -81,7 +81,7 @@ private: // these are calibration results glm::vec3 _avatarPosition { DEFAULT_AVATAR_POSITION }; // in hydra-frame glm::quat _avatarRotation; // in hydra-frame - + float _lastDistance; bool _requestReset { false }; bool _debugDrawRaw { false }; From 08a087c5aa40885185ea80371dd0536cd42fe074 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 11:19:01 -0800 Subject: [PATCH 27/41] PR comments --- libraries/ui/src/QmlWebWindowClass.cpp | 235 +++++------------- libraries/ui/src/QmlWebWindowClass.h | 14 +- .../ui/src/impl/websocketclientwrapper.cpp | 72 ------ .../ui/src/impl/websocketclientwrapper.h | 63 ----- libraries/ui/src/impl/websockettransport.cpp | 100 -------- libraries/ui/src/impl/websockettransport.h | 60 ----- 6 files changed, 73 insertions(+), 471 deletions(-) delete mode 100644 libraries/ui/src/impl/websocketclientwrapper.cpp delete mode 100644 libraries/ui/src/impl/websocketclientwrapper.h delete mode 100644 libraries/ui/src/impl/websockettransport.cpp delete mode 100644 libraries/ui/src/impl/websockettransport.h diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 2b7270edf0..eeadb361ae 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -17,32 +17,23 @@ #include #include +#include #include +#include +#include #include #include -#include "impl/websocketclientwrapper.h" -#include "impl/websockettransport.h" #include "OffscreenUi.h" -static QWebSocketServer * webChannelServer { nullptr }; -static WebSocketClientWrapper * webChannelClientWrapper { nullptr }; +QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; static QWebChannel webChannel; -static std::once_flag webChannelSetup; static const uint16_t WEB_CHANNEL_PORT = 51016; static std::atomic nextWindowId; - -void initWebChannelServer() { - std::call_once(webChannelSetup, [] { - webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); - webChannelClientWrapper = new WebSocketClientWrapper(webChannelServer); - if (!webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { - qFatal("Failed to open web socket server."); - } - QObject::connect(webChannelClientWrapper, &WebSocketClientWrapper::clientConnected, &webChannel, &QWebChannel::connectTo); - }); -} +static const char* const URL_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; void QmlScriptEventBridge::emitWebEvent(const QString& data) { QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); @@ -50,11 +41,49 @@ void QmlScriptEventBridge::emitWebEvent(const QString& data) { void QmlScriptEventBridge::emitScriptEvent(const QString& data) { QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, - Q_ARG(int, _webWindow->getWindowId()), - Q_ARG(QString, data) - ); + Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); } +class QmlWebTransport : public QWebChannelAbstractTransport { + Q_OBJECT +public: + QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { + // Translate from the websocket layer to the webchannel layer + connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); + if (error.error || !document.isObject()) { + qWarning() << "Unable to parse incoming JSON message" << message; + return; + } + emit messageReceived(document.object(), this); + }); + } + + virtual void sendMessage(const QJsonObject &message) override { + // Translate from the webchannel layer to the websocket layer + _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); + } + +private: + QWebSocket* const _webSocket; +}; + + +void QmlWebWindowClass::setupServer() { + if (!_webChannelServer) { + _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + + QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { + webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); + }); + } +} + +// Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { QmlWebWindowClass* retVal { nullptr }; const QString title = context->argument(0).toString(); @@ -69,20 +98,19 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, Q_ARG(const QString&, "QmlWebWindow.qml"), Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { - initWebChannelServer(); + setupServer(); retVal = new QmlWebWindowClass(object); webChannel.registerObject(url.toLower(), retVal); retVal->setTitle(title); retVal->setURL(url); retVal->setSize(width, height); - }) - ); + })); connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); return engine->newQObject(retVal); } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _isToolWindow(false), _windowId(++nextWindowId), _eventBridge(new QmlScriptEventBridge(this)), _qmlWindow(qmlWindow) + : _isToolWindow(false), _windowId(++nextWindowId), _qmlWindow(qmlWindow) { qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); @@ -94,7 +122,6 @@ void QmlWebWindowClass::handleNavigation(const QString& url) { DependencyManager::get()->handleLookupString(url); } - void QmlWebWindowClass::setVisible(bool visible) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); @@ -108,6 +135,10 @@ void QmlWebWindowClass::setVisible(bool visible) { } } +QQuickItem* QmlWebWindowClass::asQuickItem() const { + return dynamic_cast(_qmlWindow); +} + bool QmlWebWindowClass::isVisible() const { if (QThread::currentThread() != thread()) { bool result; @@ -115,7 +146,7 @@ bool QmlWebWindowClass::isVisible() const { return result; } - return ((QQuickItem*)_qmlWindow)->isEnabled(); + return asQuickItem()->isEnabled(); } @@ -126,7 +157,7 @@ glm::vec2 QmlWebWindowClass::getPosition() const { return result; } - return glm::vec2(((QQuickItem*)_qmlWindow)->x(), ((QQuickItem*)_qmlWindow)->y()); + return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); } @@ -136,7 +167,7 @@ void QmlWebWindowClass::setPosition(const glm::vec2& position) { return; } - ((QQuickItem*)_qmlWindow)->setPosition(QPointF(position.x, position.y)); + asQuickItem()->setPosition(QPointF(position.x, position.y)); } void QmlWebWindowClass::setPosition(int x, int y) { @@ -150,7 +181,7 @@ glm::vec2 QmlWebWindowClass::getSize() const { return result; } - return glm::vec2(((QQuickItem*)_qmlWindow)->width(), ((QQuickItem*)_qmlWindow)->height()); + return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); } void QmlWebWindowClass::setSize(const glm::vec2& size) { @@ -158,15 +189,13 @@ void QmlWebWindowClass::setSize(const glm::vec2& size) { QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); } - ((QQuickItem*)_qmlWindow)->setSize(QSizeF(size.x, size.y)); + asQuickItem()->setSize(QSizeF(size.x, size.y)); } void QmlWebWindowClass::setSize(int width, int height) { setSize(glm::vec2(width, height)); } -static const char* const URL_PROPERTY = "source"; - QString QmlWebWindowClass::getURL() const { if (QThread::currentThread() != thread()) { QString result; @@ -183,7 +212,6 @@ void QmlWebWindowClass::setURL(const QString& urlString) { _qmlWindow->setProperty(URL_PROPERTY, urlString); } -static const char* const TITLE_PROPERTY = "title"; void QmlWebWindowClass::setTitle(const QString& title) { if (QThread::currentThread() != thread()) { @@ -206,146 +234,7 @@ void QmlWebWindowClass::hasClosed() { } void QmlWebWindowClass::raise() { + // FIXME } -#if 0 - -#include - -#include - -WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) - : QObject(NULL), _eventBridge(new ScriptEventBridge(this)), _isToolWindow(isToolWindow) { - /* - if (_isToolWindow) { - ToolWindow* toolWindow = qApp->getToolWindow(); - - auto dockWidget = new QDockWidget(title, toolWindow); - dockWidget->setFeatures(QDockWidget::DockWidgetMovable); - connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged); - - _webView = new QWebView(dockWidget); - addEventBridgeToWindowObject(); - - dockWidget->setWidget(_webView); - - auto titleWidget = new QWidget(dockWidget); - dockWidget->setTitleBarWidget(titleWidget); - - toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal); - - _windowWidget = dockWidget; - } else { - auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); - dialogWidget->setWindowTitle(title); - dialogWidget->resize(width, height); - dialogWidget->installEventFilter(this); - connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); - - auto layout = new QVBoxLayout(dialogWidget); - layout->setContentsMargins(0, 0, 0, 0); - dialogWidget->setLayout(layout); - - _webView = new QWebView(dialogWidget); - - layout->addWidget(_webView); - - addEventBridgeToWindowObject(); - - _windowWidget = dialogWidget; - } - - auto style = QStyleFactory::create("fusion"); - if (style) { - _webView->setStyle(style); - } - - _webView->setPage(new DataWebPage()); - if (!url.startsWith("http") && !url.startsWith("file://")) { - _webView->setUrl(QUrl::fromLocalFile(url)); - } else { - _webView->setUrl(url); - } - connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); - connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, - this, &WebWindowClass::addEventBridgeToWindowObject); - */ -} - -void WebWindowClass::hasClosed() { - emit closed(); -} - - -void WebWindowClass::setVisible(bool visible) { -} - -QString WebWindowClass::getURL() const { - return QString(); -} - -void WebWindowClass::setURL(const QString& url) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setURL", Qt::AutoConnection, Q_ARG(QString, url)); - return; - } -} - -QSizeF WebWindowClass::getSize() const { - QSizeF size; - return size; -} - -void WebWindowClass::setSize(const QSizeF& size) { - setSize(size.width(), size.height()); -} - -void WebWindowClass::setSize(int width, int height) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::AutoConnection, Q_ARG(int, width), Q_ARG(int, height)); - return; - } -} - -glm::vec2 WebWindowClass::getPosition() const { - return glm::vec2(); -} - -void WebWindowClass::setPosition(const glm::vec2& position) { - setPosition(position.x, position.y); -} - -void WebWindowClass::setPosition(int x, int y) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::AutoConnection, Q_ARG(int, x), Q_ARG(int, y)); - return; - } -} - -void WebWindowClass::raise() { -} - -QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - WebWindowClass* retVal { nullptr }; - //QString file = context->argument(0).toString(); - //QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, - // Q_RETURN_ARG(WebWindowClass*, retVal), - // Q_ARG(const QString&, file), - // Q_ARG(QString, context->argument(1).toString()), - // Q_ARG(int, context->argument(2).toInteger()), - // Q_ARG(int, context->argument(3).toInteger()), - // Q_ARG(bool, context->argument(4).toBool())); - - //connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); - - return engine->newQObject(retVal); -} - -void WebWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::AutoConnection, Q_ARG(QString, title)); - return; - } -} - -#endif \ No newline at end of file +#include "QmlWebWindowClass.moc" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 1575ec7916..90d355e162 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -13,10 +13,13 @@ #include #include #include +#include class QScriptEngine; class QScriptContext; class QmlWebWindowClass; +class QWebSocketServer; +class QWebSocket; class QmlScriptEventBridge : public QObject { Q_OBJECT @@ -33,6 +36,7 @@ signals: private: const QmlWebWindowClass* _webWindow { nullptr }; + QWebSocket *_socket { nullptr }; }; // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping @@ -84,9 +88,13 @@ private slots: void handleNavigation(const QString& url); private: - QmlScriptEventBridge* _eventBridge; - bool _isToolWindow { false }; - QObject* _qmlWindow; + static void setupServer(); + static QWebSocketServer* _webChannelServer; + + QQuickItem* asQuickItem() const; + QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; + const bool _isToolWindow; + QObject* const _qmlWindow; const int _windowId; }; diff --git a/libraries/ui/src/impl/websocketclientwrapper.cpp b/libraries/ui/src/impl/websocketclientwrapper.cpp deleted file mode 100644 index 00ddd6e009..0000000000 --- a/libraries/ui/src/impl/websocketclientwrapper.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "websocketclientwrapper.h" - -#include - -#include "websockettransport.h" - -/*! - \brief Wrapps connected QWebSockets clients in WebSocketTransport objects. - - This code is all that is required to connect incoming WebSockets to the WebChannel. Any kind - of remote JavaScript client that supports WebSockets can thus receive messages and access the - published objects. -*/ - -QT_BEGIN_NAMESPACE - -/*! - Construct the client wrapper with the given parent. - - All clients connecting to the QWebSocketServer will be automatically wrapped - in WebSocketTransport objects. -*/ -WebSocketClientWrapper::WebSocketClientWrapper(QWebSocketServer *server, QObject *parent) - : QObject(parent) - , m_server(server) -{ - connect(server, &QWebSocketServer::newConnection, - this, &WebSocketClientWrapper::handleNewConnection); -} - -/*! - Wrap an incoming WebSocket connection in a WebSocketTransport object. -*/ -void WebSocketClientWrapper::handleNewConnection() -{ - emit clientConnected(new WebSocketTransport(m_server->nextPendingConnection())); -} - -QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websocketclientwrapper.h b/libraries/ui/src/impl/websocketclientwrapper.h deleted file mode 100644 index edb0a1b1a3..0000000000 --- a/libraries/ui/src/impl/websocketclientwrapper.h +++ /dev/null @@ -1,63 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef WEBSOCKETTRANSPORTSERVER_H -#define WEBSOCKETTRANSPORTSERVER_H - -#include - -QT_BEGIN_NAMESPACE - -class QWebSocketServer; -class WebSocketTransport; - -class WebSocketClientWrapper : public QObject -{ - Q_OBJECT - -public: - WebSocketClientWrapper(QWebSocketServer *server, QObject *parent = 0); - -Q_SIGNALS: - void clientConnected(WebSocketTransport* client); - -private Q_SLOTS: - void handleNewConnection(); - -private: - QWebSocketServer *m_server; -}; - -QT_END_NAMESPACE - -#endif // WEBSOCKETTRANSPORTSERVER_H diff --git a/libraries/ui/src/impl/websockettransport.cpp b/libraries/ui/src/impl/websockettransport.cpp deleted file mode 100644 index 8ed330c72d..0000000000 --- a/libraries/ui/src/impl/websockettransport.cpp +++ /dev/null @@ -1,100 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "websockettransport.h" - -#include -#include -#include - -#include - -/*! - \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. - - The transport delegates all messages received over the QWebSocket over its - textMessageReceived signal. Analogously, all calls to sendTextMessage will - be send over the QWebSocket to the remote client. -*/ - -QT_BEGIN_NAMESPACE - -/*! - Construct the transport object and wrap the given socket. - - The socket is also set as the parent of the transport object. -*/ -WebSocketTransport::WebSocketTransport(QWebSocket *socket) -: QWebChannelAbstractTransport(socket) -, m_socket(socket) -{ - connect(socket, &QWebSocket::textMessageReceived, - this, &WebSocketTransport::textMessageReceived); -} - -/*! - Destroys the WebSocketTransport. -*/ -WebSocketTransport::~WebSocketTransport() -{ - -} - -/*! - Serialize the JSON message and send it as a text message via the WebSocket to the client. -*/ -void WebSocketTransport::sendMessage(const QJsonObject &message) -{ - QJsonDocument doc(message); - m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); -} - -/*! - Deserialize the stringified JSON messageData and emit messageReceived. -*/ -void WebSocketTransport::textMessageReceived(const QString &messageData) -{ - QJsonParseError error; - QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); - if (error.error) { - qWarning() << "Failed to parse text message as JSON object:" << messageData - << "Error is:" << error.errorString(); - return; - } else if (!message.isObject()) { - qWarning() << "Received JSON message that is not an object: " << messageData; - return; - } - emit messageReceived(message.object(), this); -} - -QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websockettransport.h b/libraries/ui/src/impl/websockettransport.h deleted file mode 100644 index a1fdd3553a..0000000000 --- a/libraries/ui/src/impl/websockettransport.h +++ /dev/null @@ -1,60 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef WEBSOCKETTRANSPORT_H -#define WEBSOCKETTRANSPORT_H - -#include - -QT_BEGIN_NAMESPACE - -class QWebSocket; -class WebSocketTransport : public QWebChannelAbstractTransport -{ - Q_OBJECT -public: - explicit WebSocketTransport(QWebSocket *socket); - virtual ~WebSocketTransport(); - - void sendMessage(const QJsonObject &message) Q_DECL_OVERRIDE; - -private Q_SLOTS: - void textMessageReceived(const QString &message); - -private: - QWebSocket *m_socket; -}; - -QT_END_NAMESPACE - -#endif // WEBSOCKETTRANSPORT_H From 5701ad2e8cdf4f700938f033442990888c411569 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Dec 2015 12:38:50 -0800 Subject: [PATCH 28/41] update to new angular velocity --- examples/controllers/proceduralHandPoseExample.js | 2 +- libraries/controllers/src/controllers/Pose.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/controllers/proceduralHandPoseExample.js b/examples/controllers/proceduralHandPoseExample.js index 5d0b10c46a..8b735f579f 100644 --- a/examples/controllers/proceduralHandPoseExample.js +++ b/examples/controllers/proceduralHandPoseExample.js @@ -63,7 +63,7 @@ mapping.from(function() { translation: translation, rotation: rotation, velocity: { x: 0, y: 0, z: 0 }, - angularVelocity: { x: 0, y: 0, z: 0, w: 1 } + angularVelocity: { x: 0, y: 0, z: 0 } }; return pose; }).debug(true).to(Controller.Standard.LeftHand); diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index 6b2d12e5df..d9641618c1 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -53,7 +53,7 @@ namespace controller { vec3FromScriptValue(translation, pose.translation); quatFromScriptValue(rotation, pose.rotation); vec3FromScriptValue(velocity, pose.velocity); - quatFromScriptValue(angularVelocity, pose.angularVelocity); + vec3FromScriptValue(angularVelocity, pose.angularVelocity); pose.valid = true; } else { pose.valid = false; From 16dd24f3d8be0a9cbcd05bcbc44b13a7b84f4ce2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 13:02:45 -0800 Subject: [PATCH 29/41] Removing unused LGPL JS file --- interface/resources/qml/qwebchannel.js | 413 ------------------------- 1 file changed, 413 deletions(-) delete mode 100644 interface/resources/qml/qwebchannel.js diff --git a/interface/resources/qml/qwebchannel.js b/interface/resources/qml/qwebchannel.js deleted file mode 100644 index d8c28bc663..0000000000 --- a/interface/resources/qml/qwebchannel.js +++ /dev/null @@ -1,413 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2015 The Qt Company Ltd. -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -"use strict"; - -var QWebChannelMessageTypes = { - signal: 1, - propertyUpdate: 2, - init: 3, - idle: 4, - debug: 5, - invokeMethod: 6, - connectToSignal: 7, - disconnectFromSignal: 8, - setProperty: 9, - response: 10, -}; - -var QWebChannel = function(transport, initCallback) -{ - if (typeof transport !== "object" || typeof transport.send !== "function") { - console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + - " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); - return; - } - - var channel = this; - this.transport = transport; - - this.send = function(data) - { - if (typeof(data) !== "string") { - data = JSON.stringify(data); - } - channel.transport.send(data); - } - - this.transport.onmessage = function(message) - { - var data = message.data; - if (typeof data === "string") { - data = JSON.parse(data); - } - switch (data.type) { - case QWebChannelMessageTypes.signal: - channel.handleSignal(data); - break; - case QWebChannelMessageTypes.response: - channel.handleResponse(data); - break; - case QWebChannelMessageTypes.propertyUpdate: - channel.handlePropertyUpdate(data); - break; - default: - console.error("invalid message received:", message.data); - break; - } - } - - this.execCallbacks = {}; - this.execId = 0; - this.exec = function(data, callback) - { - if (!callback) { - // if no callback is given, send directly - channel.send(data); - return; - } - if (channel.execId === Number.MAX_VALUE) { - // wrap - channel.execId = Number.MIN_VALUE; - } - if (data.hasOwnProperty("id")) { - console.error("Cannot exec message with property id: " + JSON.stringify(data)); - return; - } - data.id = channel.execId++; - channel.execCallbacks[data.id] = callback; - channel.send(data); - }; - - this.objects = {}; - - this.handleSignal = function(message) - { - var object = channel.objects[message.object]; - if (object) { - object.signalEmitted(message.signal, message.args); - } else { - console.warn("Unhandled signal: " + message.object + "::" + message.signal); - } - } - - this.handleResponse = function(message) - { - if (!message.hasOwnProperty("id")) { - console.error("Invalid response message received: ", JSON.stringify(message)); - return; - } - channel.execCallbacks[message.id](message.data); - delete channel.execCallbacks[message.id]; - } - - this.handlePropertyUpdate = function(message) - { - for (var i in message.data) { - var data = message.data[i]; - var object = channel.objects[data.object]; - if (object) { - object.propertyUpdate(data.signals, data.properties); - } else { - console.warn("Unhandled property update: " + data.object + "::" + data.signal); - } - } - channel.exec({type: QWebChannelMessageTypes.idle}); - } - - this.debug = function(message) - { - channel.send({type: QWebChannelMessageTypes.debug, data: message}); - }; - - channel.exec({type: QWebChannelMessageTypes.init}, function(data) { - for (var objectName in data) { - var object = new QObject(objectName, data[objectName], channel); - } - // now unwrap properties, which might reference other registered objects - for (var objectName in channel.objects) { - channel.objects[objectName].unwrapProperties(); - } - if (initCallback) { - initCallback(channel); - } - channel.exec({type: QWebChannelMessageTypes.idle}); - }); -}; - -function QObject(name, data, webChannel) -{ - this.__id__ = name; - webChannel.objects[name] = this; - - // List of callbacks that get invoked upon signal emission - this.__objectSignals__ = {}; - - // Cache of all properties, updated when a notify signal is emitted - this.__propertyCache__ = {}; - - var object = this; - - // ---------------------------------------------------------------------- - - this.unwrapQObject = function(response) - { - if (response instanceof Array) { - // support list of objects - var ret = new Array(response.length); - for (var i = 0; i < response.length; ++i) { - ret[i] = object.unwrapQObject(response[i]); - } - return ret; - } - if (!response - || !response["__QObject*__"] - || response.id === undefined) { - return response; - } - - var objectId = response.id; - if (webChannel.objects[objectId]) - return webChannel.objects[objectId]; - - if (!response.data) { - console.error("Cannot unwrap unknown QObject " + objectId + " without data."); - return; - } - - var qObject = new QObject( objectId, response.data, webChannel ); - qObject.destroyed.connect(function() { - if (webChannel.objects[objectId] === qObject) { - delete webChannel.objects[objectId]; - // reset the now deleted QObject to an empty {} object - // just assigning {} though would not have the desired effect, but the - // below also ensures all external references will see the empty map - // NOTE: this detour is necessary to workaround QTBUG-40021 - var propertyNames = []; - for (var propertyName in qObject) { - propertyNames.push(propertyName); - } - for (var idx in propertyNames) { - delete qObject[propertyNames[idx]]; - } - } - }); - // here we are already initialized, and thus must directly unwrap the properties - qObject.unwrapProperties(); - return qObject; - } - - this.unwrapProperties = function() - { - for (var propertyIdx in object.__propertyCache__) { - object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); - } - } - - function addSignal(signalData, isPropertyNotifySignal) - { - var signalName = signalData[0]; - var signalIndex = signalData[1]; - object[signalName] = { - connect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signalName); - return; - } - - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - object.__objectSignals__[signalIndex].push(callback); - - if (!isPropertyNotifySignal && signalName !== "destroyed") { - // only required for "pure" signals, handled separately for properties in propertyUpdate - // also note that we always get notified about the destroyed signal - webChannel.exec({ - type: QWebChannelMessageTypes.connectToSignal, - object: object.__id__, - signal: signalIndex - }); - } - }, - disconnect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to disconnect from signal " + signalName); - return; - } - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - var idx = object.__objectSignals__[signalIndex].indexOf(callback); - if (idx === -1) { - console.error("Cannot find connection of signal " + signalName + " to " + callback.name); - return; - } - object.__objectSignals__[signalIndex].splice(idx, 1); - if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: QWebChannelMessageTypes.disconnectFromSignal, - object: object.__id__, - signal: signalIndex - }); - } - } - }; - } - - /** - * Invokes all callbacks for the given signalname. Also works for property notify callbacks. - */ - function invokeSignalCallbacks(signalName, signalArgs) - { - var connections = object.__objectSignals__[signalName]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, signalArgs); - }); - } - } - - this.propertyUpdate = function(signals, propertyMap) - { - // update property cache - for (var propertyIndex in propertyMap) { - var propertyValue = propertyMap[propertyIndex]; - object.__propertyCache__[propertyIndex] = propertyValue; - } - - for (var signalName in signals) { - // Invoke all callbacks, as signalEmitted() does not. This ensures the - // property cache is updated before the callbacks are invoked. - invokeSignalCallbacks(signalName, signals[signalName]); - } - } - - this.signalEmitted = function(signalName, signalArgs) - { - invokeSignalCallbacks(signalName, signalArgs); - } - - function addMethod(methodData) - { - var methodName = methodData[0]; - var methodIdx = methodData[1]; - object[methodName] = function() { - var args = []; - var callback; - for (var i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] === "function") - callback = arguments[i]; - else - args.push(arguments[i]); - } - - webChannel.exec({ - "type": QWebChannelMessageTypes.invokeMethod, - "object": object.__id__, - "method": methodIdx, - "args": args - }, function(response) { - if (response !== undefined) { - var result = object.unwrapQObject(response); - if (callback) { - (callback)(result); - } - } - }); - }; - } - - function bindGetterSetter(propertyInfo) - { - var propertyIndex = propertyInfo[0]; - var propertyName = propertyInfo[1]; - var notifySignalData = propertyInfo[2]; - // initialize property cache with current value - // NOTE: if this is an object, it is not directly unwrapped as it might - // reference other QObject that we do not know yet - object.__propertyCache__[propertyIndex] = propertyInfo[3]; - - if (notifySignalData) { - if (notifySignalData[0] === 1) { - // signal name is optimized away, reconstruct the actual name - notifySignalData[0] = propertyName + "Changed"; - } - addSignal(notifySignalData, true); - } - - Object.defineProperty(object, propertyName, { - configurable: true, - get: function () { - var propertyValue = object.__propertyCache__[propertyIndex]; - if (propertyValue === undefined) { - // This shouldn't happen - console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); - } - - return propertyValue; - }, - set: function(value) { - if (value === undefined) { - console.warn("Property setter for " + propertyName + " called with undefined value!"); - return; - } - object.__propertyCache__[propertyIndex] = value; - webChannel.exec({ - "type": QWebChannelMessageTypes.setProperty, - "object": object.__id__, - "property": propertyIndex, - "value": value - }); - } - }); - - } - - // ---------------------------------------------------------------------- - - data.methods.forEach(addMethod); - - data.properties.forEach(bindGetterSetter); - - data.signals.forEach(function(signal) { addSignal(signal, false); }); - - for (var name in data.enums) { - object[name] = data.enums[name]; - } -} - -//required for use with nodejs -if (typeof module === 'object') { - module.exports = { - QWebChannel: QWebChannel - }; -} From 4e76e9e50b9088358f6be7465d506c19174d0506 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Dec 2015 13:40:35 -0800 Subject: [PATCH 30/41] prevent nan values in measure of angularVelocity --- plugins/hifiSixense/src/SixenseManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index fb47697166..b534c09055 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -466,9 +466,10 @@ void SixenseManager::InputDevice::handlePoseEvent(float deltaTime, glm::vec3 pos samples.first.addSample(velocity); velocity = samples.first.average; - auto deltaRot = rotation * glm::conjugate(prevPose.getRotation()); + auto deltaRot = glm::normalize(rotation * glm::conjugate(prevPose.getRotation())); auto axis = glm::axis(deltaRot); auto speed = glm::angle(deltaRot) / deltaTime; + assert(!glm::isnan(speed)); angularVelocity = speed * axis; samples.second.addSample(angularVelocity); angularVelocity = samples.second.average; From 88b88a3306b049d9ebb15f6a34c35256a6ed3ac5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 13:45:07 -0800 Subject: [PATCH 31/41] Fixing warnings --- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +++++- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 +++++ libraries/ui/src/QmlWebWindowClass.cpp | 4 ++-- libraries/ui/src/QmlWebWindowClass.h | 7 ++++-- tests/ui/src/main.cpp | 22 ++++--------------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 3e879df7af..6397d30e13 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,6 +39,11 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } +void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { + _context->setShareContext(otherContext); +} + bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} \ No newline at end of file +} + diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 832119162c..b736253213 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,6 +25,12 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); + void setShareContext(QOpenGLContext* otherContext); + + QOpenGLContext* getContext() { + return _context; + } + private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index eeadb361ae..d5cdc1fde9 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -110,7 +110,7 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _isToolWindow(false), _windowId(++nextWindowId), _qmlWindow(qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) { qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); @@ -128,7 +128,7 @@ void QmlWebWindowClass::setVisible(bool visible) { return; } - auto qmlWindow = (QQuickItem*)_qmlWindow; + auto qmlWindow = asQuickItem(); if (qmlWindow->isEnabled() != visible) { qmlWindow->setEnabled(visible); emit visibilityChanged(visible); diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 90d355e162..2b563e68ba 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -93,9 +93,12 @@ private: QQuickItem* asQuickItem() const; QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; - const bool _isToolWindow; - QObject* const _qmlWindow; + + // FIXME needs to be initialized in the ctor once we have support + // for tool window panes in QML + const bool _isToolWindow { false }; const int _windowId; + QObject* const _qmlWindow; }; #endif diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 00d1f2dc00..59e7376f1b 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -26,8 +26,7 @@ #include #include -#include -#include +#include #include @@ -403,7 +402,6 @@ private: std::atomic _isFinished { false }; std::atomic _isRunning { false }; bool _wantSignals { true }; - bool _isThreaded { false }; QHash _timerFunctionMap; }; @@ -423,7 +421,7 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); class QTestWindow : public QWindow { Q_OBJECT - QOpenGLContext* _context{ nullptr }; + QOpenGLContextWrapper* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; @@ -450,7 +448,7 @@ public: setSurfaceType(QSurface::OpenGLSurface); QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); setFormat(format); - _context = new QOpenGLContext; + _context = new QOpenGLContextWrapper(); _context->setFormat(format); _context->setShareContext(_chromiumShareContext->getContext()); } @@ -464,18 +462,6 @@ public: makeCurrent(); - - { - qDebug() << (const char*)glGetString(GL_VERSION); - QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); - logger->initialize(); // initializes in the current context, i.e. ctx - logger->enableMessages(); - connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; - }); - //logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); - } - glewExperimental = true; glewInit(); glGetError(); @@ -492,7 +478,7 @@ public: auto offscreenUi = DependencyManager::set(); { - offscreenUi->create(_context); + offscreenUi->create(_context->getContext()); offscreenUi->setProxyWindow(this); connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { From 4075f355a3dfa9a0e9e5016de4570b9ab68feedc Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Dec 2015 15:44:01 -0800 Subject: [PATCH 32/41] more work on hand movement --- examples/controllers/reticleHandTest.js | 82 ++++++++++++++++--- interface/src/Application.cpp | 36 ++++++-- .../src/controllers/ScriptingInterface.h | 18 +++- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/examples/controllers/reticleHandTest.js b/examples/controllers/reticleHandTest.js index 5d0e2f238a..4132c80d85 100644 --- a/examples/controllers/reticleHandTest.js +++ b/examples/controllers/reticleHandTest.js @@ -9,23 +9,83 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -function moveReticle(pitch, yaw) { - //print("pitch:" + pitch); - //print("yaw:" + yaw); - var globalPos = Controller.getReticlePosition(); - globalPos.x += yaw; - globalPos.y += pitch; - Controller.setReticlePosition(globalPos); +function msecTimestampNow() { + var d = new Date(); + return d.getTime(); } +function length(posA, posB) { + var dx = posA.x - posB.x; + var dy = posA.y - posB.y; + var length = Math.sqrt((dx*dx) + (dy*dy)) + return length; +} + +var EXPECTED_CHANGE = 50; +var lastPos = Controller.getReticlePosition(); +function moveReticle(dX, dY) { + var globalPos = Controller.getReticlePosition(); + + // some debugging to see if position is jumping around on us... + var distanceSinceLastMove = length(lastPos, globalPos); + if (distanceSinceLastMove > EXPECTED_CHANGE) { + print("distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); + } + + if (Math.abs(dX) > EXPECTED_CHANGE) { + print("surpressing unexpectedly large change dX:" + dX + "----------------------------"); + dX = 0; + } + if (Math.abs(dY) > EXPECTED_CHANGE) { + print("surpressing unexpectedly large change dY:" + dY + "----------------------------"); + dY = 0; + } + + globalPos.x += dX; + globalPos.y += dY; + Controller.setReticlePosition(globalPos); + lastPos = globalPos; +} + +var lastTime = msecTimestampNow(); + var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; var mapping = Controller.newMapping(MAPPING_NAME); mapping.from(Controller.Standard.RightHand).peek().to(function(pose) { - var angularVelocityEADs = Quat.safeEulerAngles(pose.angularVelocity); // degrees - var yaw = isNaN(angularVelocityEADs.y) ? 0 : (-angularVelocityEADs.y / 10); - var pitch = isNaN(angularVelocityEADs.x) ? 0 : (-angularVelocityEADs.x / 10); - moveReticle(pitch, yaw); + + + // pose.angularVelocity - is the angularVelocity in a "physics" sense, that + // means the direction of the vector is the axis of symetry of rotation + // and the scale of the vector is the speed in radians/second of rotation + // around that axis. + // + // we want to deconstruct that in the portion of the rotation on the Y axis + // and make that portion move our reticle in the horizontal/X direction + // and the portion of the rotation on the X axis and make that portion + // move our reticle in the veritcle/Y direction + var xPart = -pose.angularVelocity.y; + var yPart = -pose.angularVelocity.x; + + + var MOVE_SCALE = 1; + var MSECS_PER_SECOND = 1000; + var now = msecTimestampNow(); + var secondsSinceLast = (now - lastTime) / MSECS_PER_SECOND; + lastTime = now; + + //print("secondsSinceLast:" + secondsSinceLast); + + //print("x part:" + xPart); + //print("y part:" + yPart); + + var dX = (xPart * MOVE_SCALE) / secondsSinceLast; + var dY = (yPart * MOVE_SCALE) / secondsSinceLast; + + //print("dX:" + dX); + //print("dY:" + dY); + + moveReticle(dX, dY); }); mapping.enable(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa5565417d..045b8beb40 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -686,13 +686,37 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { VrMenu::toggle(); // show context menu even on non-stereo displays } else if (action == controller::toInt(controller::Action::RETICLE_X)) { - auto globalPos = QCursor::pos(); - globalPos.setX(globalPos.x() + state); - QCursor::setPos(globalPos); + auto oldPos = QCursor::pos(); + auto newPos = oldPos; + newPos.setX(oldPos.x() + state); + QCursor::setPos(newPos); + + + // NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies, + // remove it after we're done + const float REASONABLE_CHANGE = 50.0f; + glm::vec2 oldPosG = { oldPos.x(), oldPos.y() }; + glm::vec2 newPosG = { newPos.x(), newPos.y() }; + auto distance = glm::distance(oldPosG, newPosG); + if (distance > REASONABLE_CHANGE) { + qDebug() << "Action::RETICLE_X... UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPosG << " newPos:" << newPosG; + } + } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { - auto globalPos = QCursor::pos(); - globalPos.setY(globalPos.y() + state); - QCursor::setPos(globalPos); + auto oldPos = QCursor::pos(); + auto newPos = oldPos; + newPos.setY(oldPos.y() + state); + QCursor::setPos(newPos); + + // NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies, + // remove it after we're done + const float REASONABLE_CHANGE = 50.0f; + glm::vec2 oldPosG = { oldPos.x(), oldPos.y() }; + glm::vec2 newPosG = { newPos.x(), newPos.y() }; + auto distance = glm::distance(oldPosG, newPosG); + if (distance > REASONABLE_CHANGE) { + qDebug() << "Action::RETICLE_Y... UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPosG << " newPos:" << newPosG; + } } } }); diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 9d3942cd55..52ec52e852 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -30,6 +30,7 @@ #include #include +#include #include "UserInputMapper.h" #include "StandardControls.h" @@ -88,8 +89,21 @@ namespace controller { Q_INVOKABLE QObject* parseMapping(const QString& json); Q_INVOKABLE QObject* loadMapping(const QString& jsonUrl); - Q_INVOKABLE glm::vec2 getReticlePosition() { return toGlm(QCursor::pos()); } - Q_INVOKABLE void setReticlePosition(glm::vec2 position) { QCursor::setPos(position.x, position.y); } + Q_INVOKABLE glm::vec2 getReticlePosition() { + return toGlm(QCursor::pos()); + } + Q_INVOKABLE void setReticlePosition(glm::vec2 position) { + // NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies, + // remove it after we're done + const float REASONABLE_CHANGE = 50.0f; + glm::vec2 oldPos = toGlm(QCursor::pos()); + auto distance = glm::distance(oldPos, position); + if (distance > REASONABLE_CHANGE) { + qDebug() << "Contrller::ScriptingInterface ---- UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPos << " newPos:" << position; + } + + QCursor::setPos(position.x, position.y); + } //Q_INVOKABLE bool isPrimaryButtonPressed() const; //Q_INVOKABLE glm::vec2 getPrimaryJoystickPosition() const; From a621579f17043ee5db822f183e50a6f1a017ed94 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Dec 2015 15:44:21 -0800 Subject: [PATCH 33/41] add philips version of hand movement --- examples/controllers/philipsVersion.js | 87 ++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 examples/controllers/philipsVersion.js diff --git a/examples/controllers/philipsVersion.js b/examples/controllers/philipsVersion.js new file mode 100644 index 0000000000..4ae617cf0b --- /dev/null +++ b/examples/controllers/philipsVersion.js @@ -0,0 +1,87 @@ +// +// reticleTest.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function length(posA, posB) { + var dx = posA.x - posB.x; + var dy = posA.y - posB.y; + var length = Math.sqrt((dx*dx) + (dy*dy)) + return length; +} + +var PITCH_DEADZONE = 1.0; +var PITCH_MAX = 20.0; +var YAW_DEADZONE = 1.0; +var YAW_MAX = 20.0; +var PITCH_SCALING = 10.0; +var YAW_SCALING = 10.0; + +var EXPECTED_CHANGE = 50; +var lastPos = Controller.getReticlePosition(); +function moveReticle(dY, dX) { + var globalPos = Controller.getReticlePosition(); + + // some debugging to see if position is jumping around on us... + var distanceSinceLastMove = length(lastPos, globalPos); + if (distanceSinceLastMove > EXPECTED_CHANGE) { + print("distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); + } + + if (Math.abs(dX) > EXPECTED_CHANGE) { + print("UNEXPECTED dX:" + dX + "----------------------------"); + dX = 0; + } + if (Math.abs(dY) > EXPECTED_CHANGE) { + print("UNEXPECTED dY:" + dY + "----------------------------"); + dY = 0; + } + + globalPos.x += dX; + globalPos.y += dY; + Controller.setReticlePosition(globalPos); + lastPos = globalPos; +} + + +var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; +var mapping = Controller.newMapping(MAPPING_NAME); + +var lastHandPitch = 0; +var lastHandYaw = 0; + +mapping.from(Controller.Standard.LeftHand).peek().to(function(pose) { + var handEulers = Quat.safeEulerAngles(pose.rotation); + //Vec3.print("handEulers:", handEulers); + + var handPitch = handEulers.y; + var handYaw = handEulers.x; + var changePitch = (handPitch - lastHandPitch) * PITCH_SCALING; + var changeYaw = (handYaw - lastHandYaw) * YAW_SCALING; + if (Math.abs(changePitch) > PITCH_MAX) { + print("Pitch: " + changePitch); + changePitch = 0; + } + if (Math.abs(changeYaw) > YAW_MAX) { + print("Yaw: " + changeYaw); + changeYaw = 0; + } + changePitch = Math.abs(changePitch) < PITCH_DEADZONE ? 0 : changePitch; + changeYaw = Math.abs(changeYaw) < YAW_DEADZONE ? 0 : changeYaw; + moveReticle(changePitch, changeYaw); + lastHandPitch = handPitch; + lastHandYaw = handYaw; + +}); +mapping.enable(); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); From 1f3adeb66622a0d527e4f4b2307330e60593728d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:09:38 -0800 Subject: [PATCH 34/41] Revert "Migrate QML based web views to Qt WebEngine" --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/directory.js | 2 +- examples/html/eventBridgeLoader.js | 59 -- examples/html/qmlWebTest.html | 31 - examples/tests/qmlWebTest.js | 32 - interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 63 -- interface/resources/qml/TestMenu.qml | 109 +++ interface/resources/qml/WebEntity.qml | 6 +- interface/src/Application.cpp | 17 - libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 - libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 - libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 240 ------- libraries/ui/src/QmlWebWindowClass.h | 104 --- tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 636 +++++------------- 24 files changed, 344 insertions(+), 1112 deletions(-) delete mode 100644 examples/html/eventBridgeLoader.js delete mode 100644 examples/html/qmlWebTest.html delete mode 100644 examples/tests/qmlWebTest.js delete mode 100644 interface/resources/qml/QmlWebWindow.qml delete mode 100644 libraries/ui/src/QmlWebWindowClass.cpp delete mode 100644 libraries/ui/src/QmlWebWindowClass.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 17b5d5f49d..bb0b268dd4 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/directory.js b/examples/directory.js index d2a3768051..b1fac19e8b 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js deleted file mode 100644 index b62e7d9384..0000000000 --- a/examples/html/eventBridgeLoader.js +++ /dev/null @@ -1,59 +0,0 @@ - -//public slots: -// void emitWebEvent(const QString& data); -// void emitScriptEvent(const QString& data); -// -//signals: -// void webEventReceived(const QString& data); -// void scriptEventReceived(const QString& data); -// - -EventBridgeConnectionProxy = function(parent) { - this.parent = parent; - this.realSignal = this.parent.realBridge.scriptEventReceived - this.webWindowId = this.parent.webWindow.windowId; -} - -EventBridgeConnectionProxy.prototype.connect = function(callback) { - var that = this; - this.realSignal.connect(function(id, message) { - if (id === that.webWindowId) { callback(message); } - }); -} - -EventBridgeProxy = function(webWindow) { - this.webWindow = webWindow; - this.realBridge = this.webWindow.eventBridge; - this.scriptEventReceived = new EventBridgeConnectionProxy(this); -} - -EventBridgeProxy.prototype.emitWebEvent = function(data) { - this.realBridge.emitWebEvent(data); -} - -openEventBridge = function(callback) { - EVENT_BRIDGE_URI = "ws://localhost:51016"; - socket = new WebSocket(this.EVENT_BRIDGE_URI); - - socket.onclose = function() { - console.error("web channel closed"); - }; - - socket.onerror = function(error) { - console.error("web channel error: " + error); - }; - - socket.onopen = function() { - channel = new QWebChannel(socket, function(channel) { - console.log("Document url is " + document.URL); - for(var key in channel.objects){ - console.log("registered object: " + key); - } - var webWindow = channel.objects[document.URL.toLowerCase()]; - console.log("WebWindow is " + webWindow) - eventBridgeProxy = new EventBridgeProxy(webWindow); - if (callback) { callback(eventBridgeProxy); } - }); - } -} - diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html deleted file mode 100644 index e59535701d..0000000000 --- a/examples/html/qmlWebTest.html +++ /dev/null @@ -1,31 +0,0 @@ - - -Properties - - - - - - - - - - - - - diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js deleted file mode 100644 index f905e494dc..0000000000 --- a/examples/tests/qmlWebTest.js +++ /dev/null @@ -1,32 +0,0 @@ -print("Launching web window"); - -webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); -print("JS Side window: " + webWindow); -print("JS Side bridge: " + webWindow.eventBridge); -webWindow.eventBridge.webEventReceived.connect(function(data) { - print("JS Side event received: " + data); -}); - -var titles = ["A", "B", "C"]; -var titleIndex = 0; - -Script.setInterval(function() { - webWindow.eventBridge.emitScriptEvent("JS Event sent"); - var size = webWindow.size; - var position = webWindow.position; - print("Window url: " + webWindow.url) - print("Window visible: " + webWindow.visible) - print("Window size: " + size.x + "x" + size.y) - print("Window pos: " + position.x + "x" + position.y) - webWindow.setVisible(!webWindow.visible); - webWindow.setTitle(titles[titleIndex]); - webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); - titleIndex += 1; - titleIndex %= titles.length; -}, 2 * 1000); - -Script.setTimeout(function() { - print("Closing script"); - webWindow.close(); - Script.stop(); -}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 1d9557a835..87cf1a384c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,9 +45,7 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS - Gui Multimedia Network OpenGL Qml Quick Script Svg - WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -177,17 +175,9 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL - Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg - Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets ) -# Issue causes build failure unless we add this directory. -# See https://bugreports.qt.io/browse/QTBUG-43351 -if (WIN32) - add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) -endif() - # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -219,9 +209,5 @@ else (APPLE) endif() endif (APPLE) -if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") -endif() - package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 8f3cd0e1e7..947bf739fc 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" import "styles" @@ -39,10 +39,9 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: webview.top + anchors.bottom: scrollView.top color: "white" } - Row { id: buttons spacing: 4 @@ -113,21 +112,25 @@ VrDialog { } } - WebEngineView { - id: webview - url: "http://highfidelity.com" + ScrollView { + id: scrollView anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - onLoadingChanged: { - if (loadRequest.status == WebEngineView.LoadSucceededStatus) { - addressBar.text = loadRequest.url + WebView { + id: webview + url: "http://highfidelity.com" + anchors.fill: parent + onLoadingChanged: { + if (loadRequest.status == WebView.LoadSucceededStarted) { + addressBar.text = loadRequest.url + } + } + onIconChanged: { + barIcon.source = icon } - } - onIconChanged: { - console.log("New icon: " + icon) } } } // item @@ -143,4 +146,5 @@ VrDialog { break; } } + } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 75b82342ca..012f04f1fd 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" VrDialog { @@ -18,11 +18,15 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - WebEngineView { - id: webview - objectName: "WebView" + ScrollView { anchors.fill: parent - url: infoView.url - } + WebView { + objectName: "WebView" + id: webview + url: infoView.url + anchors.fill: parent + } + } + } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 3a66c5340f..946f32e84a 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" VrDialog { @@ -24,22 +24,27 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - WebEngineView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" + + ScrollView { anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; + WebView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" + anchors.fill: parent + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; + return; + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; - return; + } } - } - } + } + + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml deleted file mode 100644 index 9664c0edb8..0000000000 --- a/interface/resources/qml/QmlWebWindow.qml +++ /dev/null @@ -1,63 +0,0 @@ - -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtWebEngine 1.1 -import QtWebChannel 1.0 -import QtWebSockets 1.0 - -import "qwebchannel.js" as WebChannel -import "controls" -import "styles" - -VrDialog { - id: root - HifiConstants { id: hifi } - title: "WebWindow" - resizable: true - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight - backgroundColor: "#7f000000" - property url source: "about:blank" - - signal navigating(string url) - - Component.onCompleted: { - enabled = true - console.log("Web Window Created " + root); - webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); - }); - - webview.loadingChanged.connect(handleWebviewLoading) - } - - - function handleWebviewLoading(loadRequest) { - var HIFI_URL_PATTERN = /^hifi:\/\//; - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var newUrl = loadRequest.url.toString(); - if (newUrl.match(HIFI_URL_PATTERN)) { - root.navigating(newUrl); - } - } - } - - Item { - id: clientArea - implicitHeight: 600 - implicitWidth: 800 - x: root.clientX - y: root.clientY - width: root.clientWidth - height: root.clientHeight - - WebEngineView { - id: webview - url: root.source - anchors.fill: parent - profile: WebEngineProfile { - httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" - } - } - } // item -} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index fe8a26e234..4d109e6298 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,7 +4,116 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { + Item { + objectName: "AllActions" + Action { + id: aboutApp + objectName: "HifiAction_" + MenuConstants.AboutApp + text: qsTr("About Interface") + } + + // + // File Menu + // + Action { + id: login + objectName: "HifiAction_" + MenuConstants.Login + text: qsTr("Login") + } + Action { + id: quit + objectName: "HifiAction_" + MenuConstants.Quit + text: qsTr("Quit") + //shortcut: StandardKey.Quit + shortcut: "Ctrl+Q" + } + + + // + // Edit menu + // + Action { + id: undo + text: "Undo" + shortcut: StandardKey.Undo + } + + Action { + id: redo + text: "Redo" + shortcut: StandardKey.Redo + } + + Action { + id: animations + objectName: "HifiAction_" + MenuConstants.Animations + text: qsTr("Animations...") + } + Action { + id: attachments + text: qsTr("Attachments...") + } + Action { + id: explode + text: qsTr("Explode on quit") + checkable: true + checked: true + } + Action { + id: freeze + text: qsTr("Freeze on quit") + checkable: true + checked: false + } + ExclusiveGroup { + Action { + id: visibleToEveryone + objectName: "HifiAction_" + MenuConstants.VisibleToEveryone + text: qsTr("Everyone") + checkable: true + checked: true + } + Action { + id: visibleToFriends + objectName: "HifiAction_" + MenuConstants.VisibleToFriends + text: qsTr("Friends") + checkable: true + } + Action { + id: visibleToNoOne + objectName: "HifiAction_" + MenuConstants.VisibleToNoOne + text: qsTr("No one") + checkable: true + } + } + } + Menu { objectName: "rootMenu"; + Menu { + title: "File" + MenuItem { action: login } + MenuItem { action: explode } + MenuItem { action: freeze } + MenuItem { action: quit } + } + Menu { + title: "Tools" + Menu { + title: "I Am Visible To" + MenuItem { action: visibleToEveryone } + MenuItem { action: visibleToFriends } + MenuItem { action: visibleToNoOne } + } + MenuItem { action: animations } + } + Menu { + title: "Long menu name top menu" + MenuItem { action: aboutApp } + } + Menu { + title: "Help" + MenuItem { action: aboutApp } + } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index ae94105672..0eb943cac7 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebEngine 1.1 +import QtWebKit 3.0 -WebEngineView { +WebView { id: root - anchors.fill: parent objectName: "webview" + anchors.fill: parent url: "about:blank" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e437a05aa3..fa5565417d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,7 +101,6 @@ #include #include #include -#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -363,17 +362,6 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; - -// FIXME hack access to the internal share context for the Chromium helper -// Normally we'd want to use QWebEngine::initialize(), but we can't because -// our primary context is a QGLWidget, which can't easily be initialized to share -// from a QOpenGLContext. -// -// So instead we create a new offscreen context to share with the QGLWidget, -// and manually set THAT to be the shared context for the Chromium helper -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); - Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -635,11 +623,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->create(_glWidget->context()->contextHandle()); - _chromiumShareContext->makeCurrent(); - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 26a564e20b..bddc7d19ae 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,13 +320,10 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - return; - } - - - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; + } } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -440,9 +437,7 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { - QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); - _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index d66cbeb285..67315b0783 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 6397d30e13..3e879df7af 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,11 +39,6 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } -void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { - _context->setShareContext(otherContext); -} - bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} - +} \ No newline at end of file diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index b736253213..832119162c 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,12 +25,6 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); - void setShareContext(QOpenGLContext* otherContext); - - QOpenGLContext* getContext() { - return _context; - } - private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index c1131765f7..3796abd92a 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5f39bee9fa..ded3db11e9 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,7 +30,6 @@ #include #include -#include #include #include @@ -351,8 +350,6 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); - - registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index cc2382926f..140ca87d0d 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp deleted file mode 100644 index d5cdc1fde9..0000000000 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ /dev/null @@ -1,240 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015-12-15 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "QmlWebWindowClass.h" - -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include "OffscreenUi.h" - -QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; -static QWebChannel webChannel; -static const uint16_t WEB_CHANNEL_PORT = 51016; -static std::atomic nextWindowId; -static const char* const URL_PROPERTY = "source"; -static const char* const TITLE_PROPERTY = "title"; -static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; - -void QmlScriptEventBridge::emitWebEvent(const QString& data) { - QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); -} - -void QmlScriptEventBridge::emitScriptEvent(const QString& data) { - QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, - Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); -} - -class QmlWebTransport : public QWebChannelAbstractTransport { - Q_OBJECT -public: - QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { - // Translate from the websocket layer to the webchannel layer - connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { - QJsonParseError error; - QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); - if (error.error || !document.isObject()) { - qWarning() << "Unable to parse incoming JSON message" << message; - return; - } - emit messageReceived(document.object(), this); - }); - } - - virtual void sendMessage(const QJsonObject &message) override { - // Translate from the webchannel layer to the websocket layer - _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); - } - -private: - QWebSocket* const _webSocket; -}; - - -void QmlWebWindowClass::setupServer() { - if (!_webChannelServer) { - _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); - if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { - qFatal("Failed to open web socket server."); - } - - QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { - webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); - }); - } -} - -// Method called by Qt scripts to create a new web window in the overlay -QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - QmlWebWindowClass* retVal { nullptr }; - const QString title = context->argument(0).toString(); - QString url = context->argument(1).toString(); - if (!url.startsWith("http") && !url.startsWith("file://")) { - url = QUrl::fromLocalFile(url).toString(); - } - const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));; - const int height = std::max(100, std::min(720, context->argument(3).toInt32()));; - - // Build the event bridge and wrapper on the main thread - QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, - Q_ARG(const QString&, "QmlWebWindow.qml"), - Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { - setupServer(); - retVal = new QmlWebWindowClass(object); - webChannel.registerObject(url.toLower(), retVal); - retVal->setTitle(title); - retVal->setURL(url); - retVal->setSize(width, height); - })); - connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); - return engine->newQObject(retVal); -} - -QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _windowId(++nextWindowId), _qmlWindow(qmlWindow) -{ - qDebug() << "Created window with ID " << _windowId; - Q_ASSERT(_qmlWindow); - Q_ASSERT(dynamic_cast(_qmlWindow)); - QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); -} - -void QmlWebWindowClass::handleNavigation(const QString& url) { - DependencyManager::get()->handleLookupString(url); -} - -void QmlWebWindowClass::setVisible(bool visible) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); - return; - } - - auto qmlWindow = asQuickItem(); - if (qmlWindow->isEnabled() != visible) { - qmlWindow->setEnabled(visible); - emit visibilityChanged(visible); - } -} - -QQuickItem* QmlWebWindowClass::asQuickItem() const { - return dynamic_cast(_qmlWindow); -} - -bool QmlWebWindowClass::isVisible() const { - if (QThread::currentThread() != thread()) { - bool result; - QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); - return result; - } - - return asQuickItem()->isEnabled(); -} - - -glm::vec2 QmlWebWindowClass::getPosition() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); -} - - -void QmlWebWindowClass::setPosition(const glm::vec2& position) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); - return; - } - - asQuickItem()->setPosition(QPointF(position.x, position.y)); -} - -void QmlWebWindowClass::setPosition(int x, int y) { - setPosition(glm::vec2(x, y)); -} - -glm::vec2 QmlWebWindowClass::getSize() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); -} - -void QmlWebWindowClass::setSize(const glm::vec2& size) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); - } - - asQuickItem()->setSize(QSizeF(size.x, size.y)); -} - -void QmlWebWindowClass::setSize(int width, int height) { - setSize(glm::vec2(width, height)); -} - -QString QmlWebWindowClass::getURL() const { - if (QThread::currentThread() != thread()) { - QString result; - QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); - return result; - } - return _qmlWindow->property(URL_PROPERTY).toString(); -} - -void QmlWebWindowClass::setURL(const QString& urlString) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); - } - _qmlWindow->setProperty(URL_PROPERTY, urlString); -} - - -void QmlWebWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); - } - - _qmlWindow->setProperty(TITLE_PROPERTY, title); -} - -void QmlWebWindowClass::close() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); - } - _qmlWindow->setProperty("destroyOnInvisible", true); - _qmlWindow->setProperty("visible", false); - _qmlWindow->deleteLater(); -} - -void QmlWebWindowClass::hasClosed() { -} - -void QmlWebWindowClass::raise() { - // FIXME -} - -#include "QmlWebWindowClass.moc" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h deleted file mode 100644 index 2b563e68ba..0000000000 --- a/libraries/ui/src/QmlWebWindowClass.h +++ /dev/null @@ -1,104 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015-12-15 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ui_QmlWebWindowClass_h -#define hifi_ui_QmlWebWindowClass_h - -#include -#include -#include -#include -#include - -class QScriptEngine; -class QScriptContext; -class QmlWebWindowClass; -class QWebSocketServer; -class QWebSocket; - -class QmlScriptEventBridge : public QObject { - Q_OBJECT -public: - QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {} - -public slots : - void emitWebEvent(const QString& data); - void emitScriptEvent(const QString& data); - -signals: - void webEventReceived(const QString& data); - void scriptEventReceived(int windowId, const QString& data); - -private: - const QmlWebWindowClass* _webWindow { nullptr }; - QWebSocket *_socket { nullptr }; -}; - -// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping -class QmlWebWindowClass : public QObject { - Q_OBJECT - Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) - Q_PROPERTY(int windowId READ getWindowId CONSTANT) - Q_PROPERTY(QString url READ getURL CONSTANT) - Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) - Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) - Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) - -public: - static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); - QmlWebWindowClass(QObject* qmlWindow); - -public slots: - bool isVisible() const; - void setVisible(bool visible); - - glm::vec2 getPosition() const; - void setPosition(const glm::vec2& position); - void setPosition(int x, int y); - - glm::vec2 getSize() const; - void setSize(const glm::vec2& size); - void setSize(int width, int height); - - QString getURL() const; - void setURL(const QString& url); - - void setTitle(const QString& title); - - // Ugh.... do not want to do - Q_INVOKABLE void raise(); - Q_INVOKABLE void close(); - Q_INVOKABLE int getWindowId() const { return _windowId; }; - Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; - -signals: - void visibilityChanged(bool visible); // Tool window - void urlChanged(); - void moved(glm::vec2 position); - void resized(QSizeF size); - void closed(); - -private slots: - void hasClosed(); - void handleNavigation(const QString& url); - -private: - static void setupServer(); - static QWebSocketServer* _webChannelServer; - - QQuickItem* asQuickItem() const; - QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; - - // FIXME needs to be initialized in the ctor once we have support - // for tool window panes in QML - const bool _isToolWindow { false }; - const int _windowId; - QObject* const _qmlWindow; -}; - -#endif diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index f94a0b85c0..8fda001e14 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,32 +2,15 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) +setup_hifi_project(Widgets OpenGL Network Qml Quick Script) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) - # Issue causes build failure unless we add this directory. - # See https://bugreports.qt.io/browse/QTBUG-43351 - add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) -# copy the resources files beside the executable -add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${PROJECT_SOURCE_DIR}/qml" - $/qml -) - - -target_glew() - -if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") -endif() - package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 59e7376f1b..18f62dc016 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,91 +1,41 @@ // -// Created by Bradley Austin Davis on 2015-04-22 -// Copyright 2013-2015 High Fidelity, Inc. +// main.cpp +// tests/render-utils/src +// +// Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include - -#include +#include "OffscreenUi.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include - -#include -#include -#include #include +#include +#include #include -#include -#include -#include -#include -#include - -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; -} - -const QString& getExamplesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; -} - -const QString& getInterfaceQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} - -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} - +#include +#include "MessageDialog.h" +#include "VrMenu.h" +#include "InfoView.h" +#include class RateCounter { std::vector times; @@ -124,394 +74,142 @@ public: }; - - -extern QOpenGLContext* qt_gl_global_share_context(); - - -static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { - if (engine.hasUncaughtException()) { - const auto backtrace = engine.uncaughtExceptionBacktrace(); - const auto exception = engine.uncaughtException().toString(); - const auto line = QString::number(engine.uncaughtExceptionLineNumber()); - engine.clearExceptions(); - - auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); - if (!backtrace.empty()) { - static const auto lineSeparator = "\n "; - message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); - } - qWarning() << qPrintable(message); - return true; - } - return false; -} - -const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); - -static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { - QString message = ""; - for (int i = 0; i < context->argumentCount(); i++) { - if (i > 0) { - message += " "; - } - message += context->argument(i).toString(); - } - qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline - - message = message.replace("\\", "\\\\") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("'", "\\'"); - engine->evaluate("Script.print('" + message + "')"); - - return QScriptValue(); -} - -class ScriptEngine : public QScriptEngine { +class MenuConstants : public QObject{ Q_OBJECT + Q_ENUMS(Item) public: - void loadFile(const QString& scriptPath) { - if (_isRunning) { - return; - } - qDebug() << "Loading script from " << scriptPath; - _fileNameString = scriptPath; + enum Item { + RenderLookAtTargets, + }; + +public: + MenuConstants(QObject* parent = nullptr) : QObject(parent) { - QFile file(scriptPath); - if (file.exists()) { - file.open(QIODevice::ReadOnly); - _scriptContents = file.readAll(); - } else { - qFatal("Missing file "); - } - runInThread(); } - - Q_INVOKABLE void stop() { - if (!_isFinished) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; - } - _isFinished = true; - if (_wantSignals) { - emit runningStateChanged(); - } - } - } - - Q_INVOKABLE void print(const QString& message) { - if (_wantSignals) { - emit printedMessage(message); - } - } - - Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { - // create the timer, add it to the map, and start it - QTimer* newTimer = new QTimer(this); - newTimer->setSingleShot(isSingleShot); - - connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); - - // make sure the timer stops when the script does - connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); - - _timerFunctionMap.insert(newTimer, function); - - newTimer->start(intervalMS); - return newTimer; - } - - Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { - return setupTimerWithInterval(function, intervalMS, false); - } - - Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { - return setupTimerWithInterval(function, timeoutMS, true); - } -private: - - void runInThread() { - QThread* workerThread = new QThread(); - connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); - connect(workerThread, &QThread::started, this, &ScriptEngine::run); - connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); - connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); - moveToThread(workerThread); - workerThread->start(); - } - - void init() { - _isInitialized = true; - registerMetaTypes(this); - registerGlobalObject("Script", this); - qScriptRegisterSequenceMetaType>(this); - qScriptRegisterSequenceMetaType>(this); - globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); - QScriptValue printConstructorValue = newFunction(debugPrint); - globalObject().setProperty("print", printConstructorValue); - } - - void timerFired() { - QTimer* callingTimer = reinterpret_cast(sender()); - QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); - - if (!callingTimer->isActive()) { - // this timer is done, we can kill it - _timerFunctionMap.remove(callingTimer); - delete callingTimer; - } - - // call the associated JS function, if it exists - if (timerFunction.isValid()) { - timerFunction.call(); - } - } - - - void run() { - if (!_isInitialized) { - init(); - } - - _isRunning = true; - if (_wantSignals) { - emit runningStateChanged(); - } - - QScriptValue result = evaluate(_scriptContents, _fileNameString); - QElapsedTimer startTime; - startTime.start(); - - int thisFrame = 0; - - qint64 lastUpdate = usecTimestampNow(); - - while (!_isFinished) { - int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec - if (usecToSleep > 0) { - usleep(usecToSleep); - } - - if (_isFinished) { - break; - } - - QCoreApplication::processEvents(); - if (_isFinished) { - break; - } - - qint64 now = usecTimestampNow(); - float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; - if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } - } - lastUpdate = now; - - // Debug and clear exceptions - hadUncaughtExceptions(*this, _fileNameString); - } - - if (_wantSignals) { - emit scriptEnding(); - } - - if (_wantSignals) { - emit finished(_fileNameString, this); - } - - _isRunning = false; - - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } - } - - void registerGlobalObject(const QString& name, QObject* object) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerGlobalObject", - Q_ARG(const QString&, name), - Q_ARG(QObject*, object)); - return; - } - if (!globalObject().property(name).isValid()) { - if (object) { - QScriptValue value = newQObject(object); - globalObject().setProperty(name, value); - } else { - globalObject().setProperty(name, QScriptValue()); - } - } - } - - void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerFunction", - Q_ARG(const QString&, name), - Q_ARG(QScriptEngine::FunctionSignature, functionSignature), - Q_ARG(int, numArguments)); - return; - } - QScriptValue scriptFun = newFunction(functionSignature, numArguments); - globalObject().setProperty(name, scriptFun); - } - - void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerFunction", - Q_ARG(const QString&, name), - Q_ARG(QScriptEngine::FunctionSignature, functionSignature), - Q_ARG(int, numArguments)); - return; - } - - QScriptValue object = globalObject().property(parent); - if (object.isValid()) { - QScriptValue scriptFun = newFunction(functionSignature, numArguments); - object.setProperty(name, scriptFun); - } - } - -signals: - void scriptLoaded(const QString& scriptFilename); - void errorLoadingScript(const QString& scriptFilename); - void update(float deltaTime); - void scriptEnding(); - void finished(const QString& fileNameString, ScriptEngine* engine); - void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message); - void errorMessage(const QString& message); - void runningStateChanged(); - void evaluationFinished(QScriptValue result, bool isException); - void loadScript(const QString& scriptName, bool isUserLoaded); - void reloadScript(const QString& scriptName, bool isUserLoaded); - void doneRunning(); - - -private: - QString _scriptContents; - QString _fileNameString; - QString _parentURL; - bool _isInitialized { false }; - std::atomic _isFinished { false }; - std::atomic _isRunning { false }; - bool _wantSignals { true }; - QHash _timerFunctionMap; }; - - -ScriptEngine* loadScript(const QString& scriptFilename) { - ScriptEngine* scriptEngine = new ScriptEngine(); - scriptEngine->loadFile(scriptFilename); - return scriptEngine; +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; } -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +const QString& getQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow { +class QTestWindow : public QWindow, private QOpenGLFunctions { Q_OBJECT - QOpenGLContextWrapper* _context{ nullptr }; + QOpenGLContext* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; - ProgramPtr _program; - ShapeWrapperPtr _plane; - QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { - _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); + connect(&_timer, &QTimer::timeout, [=] { + draw(); + }); - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->create(); - _chromiumShareContext->makeCurrent(); - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + DependencyManager::set(); + setSurfaceType(QSurface::OpenGLSurface); - { - setSurfaceType(QSurface::OpenGLSurface); - QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); - setFormat(format); - _context = new QOpenGLContextWrapper(); - _context->setFormat(format); - _context->setShareContext(_chromiumShareContext->getContext()); - } + QSurfaceFormat format; + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + format.setVersion(4, 1); + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); + format.setOption(QSurfaceFormat::DebugContext); + setFormat(format); + _context = new QOpenGLContext; + _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); + initializeOpenGLFunctions(); + + { + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); + logger->initialize(); // initializes in the current context, i.e. ctx + logger->enableMessages(); + connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { + qDebug() << debugMessage; + }); + // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + } + + qDebug() << (const char*)this->glGetString(GL_VERSION); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glDisable(GL_DEPTH_TEST); + + MessageDialog::registerType(); + VrMenu::registerType(); + InfoView::registerType(); + + + auto offscreenUi = DependencyManager::get(); + offscreenUi->create(_context); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); makeCurrent(); - glewExperimental = true; - glewInit(); - glGetError(); - - using namespace oglplus; - Context::Enable(Capability::Blend); - Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); - Context::Disable(Capability::DepthTest); - Context::Disable(Capability::CullFace); - Context::ClearColor(0.2f, 0.2f, 0.2f, 1); - - MessageDialog::registerType(); - InfoView::registerType(); - - auto offscreenUi = DependencyManager::set(); - { - offscreenUi->create(_context->getContext()); - offscreenUi->setProxyWindow(this); - - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); - - makeCurrent(); - } - - - auto primaryScreen = QGuiApplication::primaryScreen(); - auto targetScreen = primaryScreen; - auto screens = QGuiApplication::screens(); - if (screens.size() > 1) { - for (auto screen : screens) { - if (screen != targetScreen) { - targetScreen = screen; - break; - } - } - } - auto rect = targetScreen->availableGeometry(); - rect.setWidth(rect.width() * 0.8f); - rect.setHeight(rect.height() * 0.8f); - rect.moveTo(QPoint(20, 20)); + offscreenUi->setProxyWindow(this); + QDesktopWidget* desktop = QApplication::desktop(); + QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); + int height = rect.height(); + //rect.setHeight(height / 2); + rect.setY(rect.y() + height / 2); setGeometry(rect); +// setFramePosition(QPoint(-1000, 0)); +// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); + offscreenUi->load(QUrl("TestMenu.qml")); + // Requires a root menu to have been loaded before it can load + VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -529,35 +227,16 @@ private: } makeCurrent(); - auto error = glGetError(); - if (error != GL_NO_ERROR) { - qDebug() << "GL error in entering draw " << error; - } + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); - using namespace oglplus; - Context::Clear().ColorBuffer().DepthBuffer(); - ivec2 size(_size.width(), _size.height()); - size *= devicePixelRatio(); - size = glm::max(size, ivec2(100, 100)); - Context::Viewport(size.x, size.y); - if (!_program) { - _program = loadDefaultShader(); - _plane = loadPlane(_program); - } + renderQml(); - if (testQmlTexture > 0) { - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - - _program->Bind(); - _plane->Use(); - _plane->Draw(); _context->swapBuffers(this); + glFinish(); fps.increment(); - if (fps.elapsed() >= 10.0f) { + if (fps.elapsed() >= 2.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -567,6 +246,8 @@ private: _context->makeCurrent(this); } + void renderQml(); + void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -588,13 +269,11 @@ protected: offscreenUi->load("Browser.qml"); } break; - - case Qt::Key_J: + case Qt::Key_L: if (event->modifiers() & Qt::CTRL) { - loadScript(getExamplesDir() + "tests/qmlWebTest.js"); + InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); } break; - case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -602,9 +281,22 @@ protected: }); } break; + case Qt::Key_J: + if (event->modifiers() & Qt::CTRL) { + auto offscreenUi = DependencyManager::get(); + rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); + QMetaObject::invokeMethod(rootMenu, "popup"); + } + break; } QWindow::keyPressEvent(event); } + QQmlContext* menuContext{ nullptr }; + void keyReleaseEvent(QKeyEvent *event) override { + if (_altPressed && Qt::Key_Alt == event->key()) { + VrMenu::toggle(); + } + } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -616,26 +308,40 @@ protected: } }; +void QTestWindow::renderQml() { + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + if (testQmlTexture > 0) { + glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + glBegin(GL_QUADS); + { + glTexCoord2f(0, 0); + glVertex2f(-1, -1); + glTexCoord2f(0, 1); + glVertex2f(-1, 1); + glTexCoord2f(1, 1); + glVertex2f(1, 1); + glTexCoord2f(1, 0); + glVertex2f(1, -1); + } + glEnd(); +} + + const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = message; - -#ifdef Q_OS_WIN - if (!logMessage.isEmpty()) { - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); - } -#endif -} - - int main(int argc, char** argv) { - QGuiApplication app(argc, argv); - qInstallMessageHandler(messageHandler); + QApplication app(argc, argv); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From 6098c4a1d67ebb5725c7c5ebec3280df5e3baad9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:18:46 -0800 Subject: [PATCH 35/41] Revert "Revert "Migrate QML based web views to Qt WebEngine"" This reverts commit 1f3adeb66622a0d527e4f4b2307330e60593728d. --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/directory.js | 2 +- examples/html/eventBridgeLoader.js | 59 ++ examples/html/qmlWebTest.html | 31 + examples/tests/qmlWebTest.js | 32 + interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 63 ++ interface/resources/qml/TestMenu.qml | 109 --- interface/resources/qml/WebEntity.qml | 6 +- interface/src/Application.cpp | 17 + libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 + libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 + libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 240 +++++++ libraries/ui/src/QmlWebWindowClass.h | 104 +++ tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 626 +++++++++++++----- 24 files changed, 1107 insertions(+), 339 deletions(-) create mode 100644 examples/html/eventBridgeLoader.js create mode 100644 examples/html/qmlWebTest.html create mode 100644 examples/tests/qmlWebTest.js create mode 100644 interface/resources/qml/QmlWebWindow.qml create mode 100644 libraries/ui/src/QmlWebWindowClass.cpp create mode 100644 libraries/ui/src/QmlWebWindowClass.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index bb0b268dd4..17b5d5f49d 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/directory.js b/examples/directory.js index b1fac19e8b..d2a3768051 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js new file mode 100644 index 0000000000..b62e7d9384 --- /dev/null +++ b/examples/html/eventBridgeLoader.js @@ -0,0 +1,59 @@ + +//public slots: +// void emitWebEvent(const QString& data); +// void emitScriptEvent(const QString& data); +// +//signals: +// void webEventReceived(const QString& data); +// void scriptEventReceived(const QString& data); +// + +EventBridgeConnectionProxy = function(parent) { + this.parent = parent; + this.realSignal = this.parent.realBridge.scriptEventReceived + this.webWindowId = this.parent.webWindow.windowId; +} + +EventBridgeConnectionProxy.prototype.connect = function(callback) { + var that = this; + this.realSignal.connect(function(id, message) { + if (id === that.webWindowId) { callback(message); } + }); +} + +EventBridgeProxy = function(webWindow) { + this.webWindow = webWindow; + this.realBridge = this.webWindow.eventBridge; + this.scriptEventReceived = new EventBridgeConnectionProxy(this); +} + +EventBridgeProxy.prototype.emitWebEvent = function(data) { + this.realBridge.emitWebEvent(data); +} + +openEventBridge = function(callback) { + EVENT_BRIDGE_URI = "ws://localhost:51016"; + socket = new WebSocket(this.EVENT_BRIDGE_URI); + + socket.onclose = function() { + console.error("web channel closed"); + }; + + socket.onerror = function(error) { + console.error("web channel error: " + error); + }; + + socket.onopen = function() { + channel = new QWebChannel(socket, function(channel) { + console.log("Document url is " + document.URL); + for(var key in channel.objects){ + console.log("registered object: " + key); + } + var webWindow = channel.objects[document.URL.toLowerCase()]; + console.log("WebWindow is " + webWindow) + eventBridgeProxy = new EventBridgeProxy(webWindow); + if (callback) { callback(eventBridgeProxy); } + }); + } +} + diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html new file mode 100644 index 0000000000..e59535701d --- /dev/null +++ b/examples/html/qmlWebTest.html @@ -0,0 +1,31 @@ + + +Properties + + + + + + + + + + + + + diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js new file mode 100644 index 0000000000..f905e494dc --- /dev/null +++ b/examples/tests/qmlWebTest.js @@ -0,0 +1,32 @@ +print("Launching web window"); + +webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +print("JS Side window: " + webWindow); +print("JS Side bridge: " + webWindow.eventBridge); +webWindow.eventBridge.webEventReceived.connect(function(data) { + print("JS Side event received: " + data); +}); + +var titles = ["A", "B", "C"]; +var titleIndex = 0; + +Script.setInterval(function() { + webWindow.eventBridge.emitScriptEvent("JS Event sent"); + var size = webWindow.size; + var position = webWindow.position; + print("Window url: " + webWindow.url) + print("Window visible: " + webWindow.visible) + print("Window size: " + size.x + "x" + size.y) + print("Window pos: " + position.x + "x" + position.y) + webWindow.setVisible(!webWindow.visible); + webWindow.setTitle(titles[titleIndex]); + webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); + titleIndex += 1; + titleIndex %= titles.length; +}, 2 * 1000); + +Script.setTimeout(function() { + print("Closing script"); + webWindow.close(); + Script.stop(); +}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87cf1a384c..1d9557a835 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,7 +45,9 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS + Gui Multimedia Network OpenGL Qml Quick Script Svg + WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -175,9 +177,17 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL + Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg + Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets ) +# Issue causes build failure unless we add this directory. +# See https://bugreports.qt.io/browse/QTBUG-43351 +if (WIN32) + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) +endif() + # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -209,5 +219,9 @@ else (APPLE) endif() endif (APPLE) +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") +endif() + package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 947bf739fc..8f3cd0e1e7 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" import "styles" @@ -39,9 +39,10 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: scrollView.top + anchors.bottom: webview.top color: "white" } + Row { id: buttons spacing: 4 @@ -112,26 +113,22 @@ VrDialog { } } - ScrollView { - id: scrollView + WebEngineView { + id: webview + url: "http://highfidelity.com" anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - WebView { - id: webview - url: "http://highfidelity.com" - anchors.fill: parent - onLoadingChanged: { - if (loadRequest.status == WebView.LoadSucceededStarted) { - addressBar.text = loadRequest.url - } - } - onIconChanged: { - barIcon.source = icon + onLoadingChanged: { + if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + addressBar.text = loadRequest.url } } + onIconChanged: { + console.log("New icon: " + icon) + } } } // item @@ -146,5 +143,4 @@ VrDialog { break; } } - } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 012f04f1fd..75b82342ca 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -18,15 +18,11 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - ScrollView { + WebEngineView { + id: webview + objectName: "WebView" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: infoView.url - anchors.fill: parent - } - } - + url: infoView.url + } } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 946f32e84a..3a66c5340f 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -24,27 +24,22 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - - ScrollView { + WebEngineView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" - anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; - return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; return; - } + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; + return; } - } - - } + } + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml new file mode 100644 index 0000000000..9664c0edb8 --- /dev/null +++ b/interface/resources/qml/QmlWebWindow.qml @@ -0,0 +1,63 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtWebSockets 1.0 + +import "qwebchannel.js" as WebChannel +import "controls" +import "styles" + +VrDialog { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + backgroundColor: "#7f000000" + property url source: "about:blank" + + signal navigating(string url) + + Component.onCompleted: { + enabled = true + console.log("Web Window Created " + root); + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + webview.loadingChanged.connect(handleWebviewLoading) + } + + + function handleWebviewLoading(loadRequest) { + var HIFI_URL_PATTERN = /^hifi:\/\//; + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var newUrl = loadRequest.url.toString(); + if (newUrl.match(HIFI_URL_PATTERN)) { + root.navigating(newUrl); + } + } + } + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + + WebEngineView { + id: webview + url: root.source + anchors.fill: parent + profile: WebEngineProfile { + httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" + } + } + } // item +} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index 4d109e6298..fe8a26e234 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,116 +4,7 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { - Item { - objectName: "AllActions" - Action { - id: aboutApp - objectName: "HifiAction_" + MenuConstants.AboutApp - text: qsTr("About Interface") - } - - // - // File Menu - // - Action { - id: login - objectName: "HifiAction_" + MenuConstants.Login - text: qsTr("Login") - } - Action { - id: quit - objectName: "HifiAction_" + MenuConstants.Quit - text: qsTr("Quit") - //shortcut: StandardKey.Quit - shortcut: "Ctrl+Q" - } - - - // - // Edit menu - // - Action { - id: undo - text: "Undo" - shortcut: StandardKey.Undo - } - - Action { - id: redo - text: "Redo" - shortcut: StandardKey.Redo - } - - Action { - id: animations - objectName: "HifiAction_" + MenuConstants.Animations - text: qsTr("Animations...") - } - Action { - id: attachments - text: qsTr("Attachments...") - } - Action { - id: explode - text: qsTr("Explode on quit") - checkable: true - checked: true - } - Action { - id: freeze - text: qsTr("Freeze on quit") - checkable: true - checked: false - } - ExclusiveGroup { - Action { - id: visibleToEveryone - objectName: "HifiAction_" + MenuConstants.VisibleToEveryone - text: qsTr("Everyone") - checkable: true - checked: true - } - Action { - id: visibleToFriends - objectName: "HifiAction_" + MenuConstants.VisibleToFriends - text: qsTr("Friends") - checkable: true - } - Action { - id: visibleToNoOne - objectName: "HifiAction_" + MenuConstants.VisibleToNoOne - text: qsTr("No one") - checkable: true - } - } - } - Menu { objectName: "rootMenu"; - Menu { - title: "File" - MenuItem { action: login } - MenuItem { action: explode } - MenuItem { action: freeze } - MenuItem { action: quit } - } - Menu { - title: "Tools" - Menu { - title: "I Am Visible To" - MenuItem { action: visibleToEveryone } - MenuItem { action: visibleToFriends } - MenuItem { action: visibleToNoOne } - } - MenuItem { action: animations } - } - Menu { - title: "Long menu name top menu" - MenuItem { action: aboutApp } - } - Menu { - title: "Help" - MenuItem { action: aboutApp } - } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index 0eb943cac7..ae94105672 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 -WebView { +WebEngineView { id: root - objectName: "webview" anchors.fill: parent + objectName: "webview" url: "about:blank" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa5565417d..e437a05aa3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,7 @@ #include #include #include +#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -362,6 +363,17 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); + Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -623,6 +635,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(_glWidget->context()->contextHandle()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index bddc7d19ae..26a564e20b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,10 +320,13 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; - } + return; + } + + + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -437,7 +440,9 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { + QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); + _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 67315b0783..d66cbeb285 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 3e879df7af..6397d30e13 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,6 +39,11 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } +void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { + _context->setShareContext(otherContext); +} + bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} \ No newline at end of file +} + diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 832119162c..b736253213 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,6 +25,12 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); + void setShareContext(QOpenGLContext* otherContext); + + QOpenGLContext* getContext() { + return _context; + } + private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 3796abd92a..c1131765f7 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ded3db11e9..5f39bee9fa 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -350,6 +351,8 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); + + registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 140ca87d0d..cc2382926f 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp new file mode 100644 index 0000000000..d5cdc1fde9 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -0,0 +1,240 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "QmlWebWindowClass.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "OffscreenUi.h" + +QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; +static QWebChannel webChannel; +static const uint16_t WEB_CHANNEL_PORT = 51016; +static std::atomic nextWindowId; +static const char* const URL_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; + +void QmlScriptEventBridge::emitWebEvent(const QString& data) { + QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); +} + +void QmlScriptEventBridge::emitScriptEvent(const QString& data) { + QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, + Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); +} + +class QmlWebTransport : public QWebChannelAbstractTransport { + Q_OBJECT +public: + QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { + // Translate from the websocket layer to the webchannel layer + connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); + if (error.error || !document.isObject()) { + qWarning() << "Unable to parse incoming JSON message" << message; + return; + } + emit messageReceived(document.object(), this); + }); + } + + virtual void sendMessage(const QJsonObject &message) override { + // Translate from the webchannel layer to the websocket layer + _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); + } + +private: + QWebSocket* const _webSocket; +}; + + +void QmlWebWindowClass::setupServer() { + if (!_webChannelServer) { + _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + + QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { + webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); + }); + } +} + +// Method called by Qt scripts to create a new web window in the overlay +QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + QmlWebWindowClass* retVal { nullptr }; + const QString title = context->argument(0).toString(); + QString url = context->argument(1).toString(); + if (!url.startsWith("http") && !url.startsWith("file://")) { + url = QUrl::fromLocalFile(url).toString(); + } + const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));; + const int height = std::max(100, std::min(720, context->argument(3).toInt32()));; + + // Build the event bridge and wrapper on the main thread + QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, "QmlWebWindow.qml"), + Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + setupServer(); + retVal = new QmlWebWindowClass(object); + webChannel.registerObject(url.toLower(), retVal); + retVal->setTitle(title); + retVal->setURL(url); + retVal->setSize(width, height); + })); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); + return engine->newQObject(retVal); +} + +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); + QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); +} + +void QmlWebWindowClass::handleNavigation(const QString& url) { + DependencyManager::get()->handleLookupString(url); +} + +void QmlWebWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); + return; + } + + auto qmlWindow = asQuickItem(); + if (qmlWindow->isEnabled() != visible) { + qmlWindow->setEnabled(visible); + emit visibilityChanged(visible); + } +} + +QQuickItem* QmlWebWindowClass::asQuickItem() const { + return dynamic_cast(_qmlWindow); +} + +bool QmlWebWindowClass::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + return result; + } + + return asQuickItem()->isEnabled(); +} + + +glm::vec2 QmlWebWindowClass::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); +} + + +void QmlWebWindowClass::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); + return; + } + + asQuickItem()->setPosition(QPointF(position.x, position.y)); +} + +void QmlWebWindowClass::setPosition(int x, int y) { + setPosition(glm::vec2(x, y)); +} + +glm::vec2 QmlWebWindowClass::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); +} + +void QmlWebWindowClass::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); + } + + asQuickItem()->setSize(QSizeF(size.x, size.y)); +} + +void QmlWebWindowClass::setSize(int width, int height) { + setSize(glm::vec2(width, height)); +} + +QString QmlWebWindowClass::getURL() const { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); + return result; + } + return _qmlWindow->property(URL_PROPERTY).toString(); +} + +void QmlWebWindowClass::setURL(const QString& urlString) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); + } + _qmlWindow->setProperty(URL_PROPERTY, urlString); +} + + +void QmlWebWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); + } + + _qmlWindow->setProperty(TITLE_PROPERTY, title); +} + +void QmlWebWindowClass::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); + } + _qmlWindow->setProperty("destroyOnInvisible", true); + _qmlWindow->setProperty("visible", false); + _qmlWindow->deleteLater(); +} + +void QmlWebWindowClass::hasClosed() { +} + +void QmlWebWindowClass::raise() { + // FIXME +} + +#include "QmlWebWindowClass.moc" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h new file mode 100644 index 0000000000..2b563e68ba --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -0,0 +1,104 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ui_QmlWebWindowClass_h +#define hifi_ui_QmlWebWindowClass_h + +#include +#include +#include +#include +#include + +class QScriptEngine; +class QScriptContext; +class QmlWebWindowClass; +class QWebSocketServer; +class QWebSocket; + +class QmlScriptEventBridge : public QObject { + Q_OBJECT +public: + QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {} + +public slots : + void emitWebEvent(const QString& data); + void emitScriptEvent(const QString& data); + +signals: + void webEventReceived(const QString& data); + void scriptEventReceived(int windowId, const QString& data); + +private: + const QmlWebWindowClass* _webWindow { nullptr }; + QWebSocket *_socket { nullptr }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWebWindowClass : public QObject { + Q_OBJECT + Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) + Q_PROPERTY(int windowId READ getWindowId CONSTANT) + Q_PROPERTY(QString url READ getURL CONSTANT) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) + +public: + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + QmlWebWindowClass(QObject* qmlWindow); + +public slots: + bool isVisible() const; + void setVisible(bool visible); + + glm::vec2 getPosition() const; + void setPosition(const glm::vec2& position); + void setPosition(int x, int y); + + glm::vec2 getSize() const; + void setSize(const glm::vec2& size); + void setSize(int width, int height); + + QString getURL() const; + void setURL(const QString& url); + + void setTitle(const QString& title); + + // Ugh.... do not want to do + Q_INVOKABLE void raise(); + Q_INVOKABLE void close(); + Q_INVOKABLE int getWindowId() const { return _windowId; }; + Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; + +signals: + void visibilityChanged(bool visible); // Tool window + void urlChanged(); + void moved(glm::vec2 position); + void resized(QSizeF size); + void closed(); + +private slots: + void hasClosed(); + void handleNavigation(const QString& url); + +private: + static void setupServer(); + static QWebSocketServer* _webChannelServer; + + QQuickItem* asQuickItem() const; + QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; + + // FIXME needs to be initialized in the ctor once we have support + // for tool window panes in QML + const bool _isToolWindow { false }; + const int _windowId; + QObject* const _qmlWindow; +}; + +#endif diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 8fda001e14..f94a0b85c0 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,15 +2,32 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Widgets OpenGL Network Qml Quick Script) +setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) + # Issue causes build failure unless we add this directory. + # See https://bugreports.qt.io/browse/QTBUG-43351 + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) +# copy the resources files beside the executable +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/qml" + $/qml +) + + +target_glew() + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") +endif() + package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 18f62dc016..59e7376f1b 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,41 +1,91 @@ // -// main.cpp -// tests/render-utils/src -// -// Copyright 2014 High Fidelity, Inc. +// Created by Bradley Austin Davis on 2015-04-22 +// Copyright 2013-2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "OffscreenUi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include #include -#include -#include "MessageDialog.h" -#include "VrMenu.h" -#include "InfoView.h" -#include +#include +#include +#include +#include +#include +#include + +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getExamplesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getInterfaceQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} + +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} + class RateCounter { std::vector times; @@ -74,142 +124,394 @@ public: }; -class MenuConstants : public QObject{ - Q_OBJECT - Q_ENUMS(Item) -public: - enum Item { - RenderLookAtTargets, - }; -public: - MenuConstants(QObject* parent = nullptr) : QObject(parent) { +extern QOpenGLContext* qt_gl_global_share_context(); + +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qWarning() << qPrintable(message); + return true; } + return false; +} + +const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); + +static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { + QString message = ""; + for (int i = 0; i < context->argumentCount(); i++) { + if (i > 0) { + message += " "; + } + message += context->argument(i).toString(); + } + qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline + + message = message.replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("'", "\\'"); + engine->evaluate("Script.print('" + message + "')"); + + return QScriptValue(); +} + +class ScriptEngine : public QScriptEngine { + Q_OBJECT + +public: + void loadFile(const QString& scriptPath) { + if (_isRunning) { + return; + } + qDebug() << "Loading script from " << scriptPath; + _fileNameString = scriptPath; + + QFile file(scriptPath); + if (file.exists()) { + file.open(QIODevice::ReadOnly); + _scriptContents = file.readAll(); + } else { + qFatal("Missing file "); + } + runInThread(); + } + + Q_INVOKABLE void stop() { + if (!_isFinished) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } + _isFinished = true; + if (_wantSignals) { + emit runningStateChanged(); + } + } + } + + Q_INVOKABLE void print(const QString& message) { + if (_wantSignals) { + emit printedMessage(message); + } + } + + Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { + // create the timer, add it to the map, and start it + QTimer* newTimer = new QTimer(this); + newTimer->setSingleShot(isSingleShot); + + connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); + + // make sure the timer stops when the script does + connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); + + _timerFunctionMap.insert(newTimer, function); + + newTimer->start(intervalMS); + return newTimer; + } + + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { + return setupTimerWithInterval(function, intervalMS, false); + } + + Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { + return setupTimerWithInterval(function, timeoutMS, true); + } +private: + + void runInThread() { + QThread* workerThread = new QThread(); + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + connect(workerThread, &QThread::started, this, &ScriptEngine::run); + connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + moveToThread(workerThread); + workerThread->start(); + } + + void init() { + _isInitialized = true; + registerMetaTypes(this); + registerGlobalObject("Script", this); + qScriptRegisterSequenceMetaType>(this); + qScriptRegisterSequenceMetaType>(this); + globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); + QScriptValue printConstructorValue = newFunction(debugPrint); + globalObject().setProperty("print", printConstructorValue); + } + + void timerFired() { + QTimer* callingTimer = reinterpret_cast(sender()); + QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); + + if (!callingTimer->isActive()) { + // this timer is done, we can kill it + _timerFunctionMap.remove(callingTimer); + delete callingTimer; + } + + // call the associated JS function, if it exists + if (timerFunction.isValid()) { + timerFunction.call(); + } + } + + + void run() { + if (!_isInitialized) { + init(); + } + + _isRunning = true; + if (_wantSignals) { + emit runningStateChanged(); + } + + QScriptValue result = evaluate(_scriptContents, _fileNameString); + QElapsedTimer startTime; + startTime.start(); + + int thisFrame = 0; + + qint64 lastUpdate = usecTimestampNow(); + + while (!_isFinished) { + int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec + if (usecToSleep > 0) { + usleep(usecToSleep); + } + + if (_isFinished) { + break; + } + + QCoreApplication::processEvents(); + if (_isFinished) { + break; + } + + qint64 now = usecTimestampNow(); + float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; + if (!_isFinished) { + if (_wantSignals) { + emit update(deltaTime); + } + } + lastUpdate = now; + + // Debug and clear exceptions + hadUncaughtExceptions(*this, _fileNameString); + } + + if (_wantSignals) { + emit scriptEnding(); + } + + if (_wantSignals) { + emit finished(_fileNameString, this); + } + + _isRunning = false; + + if (_wantSignals) { + emit runningStateChanged(); + emit doneRunning(); + } + } + + void registerGlobalObject(const QString& name, QObject* object) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + if (!globalObject().property(name).isValid()) { + if (object) { + QScriptValue value = newQObject(object); + globalObject().setProperty(name, value); + } else { + globalObject().setProperty(name, QScriptValue()); + } + } + } + + void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + globalObject().setProperty(name, scriptFun); + } + + void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + object.setProperty(name, scriptFun); + } + } + +signals: + void scriptLoaded(const QString& scriptFilename); + void errorLoadingScript(const QString& scriptFilename); + void update(float deltaTime); + void scriptEnding(); + void finished(const QString& fileNameString, ScriptEngine* engine); + void cleanupMenuItem(const QString& menuItemString); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void runningStateChanged(); + void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName, bool isUserLoaded); + void reloadScript(const QString& scriptName, bool isUserLoaded); + void doneRunning(); + + +private: + QString _scriptContents; + QString _fileNameString; + QString _parentURL; + bool _isInitialized { false }; + std::atomic _isFinished { false }; + std::atomic _isRunning { false }; + bool _wantSignals { true }; + QHash _timerFunctionMap; }; -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; + + +ScriptEngine* loadScript(const QString& scriptFilename) { + ScriptEngine* scriptEngine = new ScriptEngine(); + scriptEngine->loadFile(scriptFilename); + return scriptEngine; } -const QString& getQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow, private QOpenGLFunctions { +class QTestWindow : public QWindow { Q_OBJECT - QOpenGLContext* _context{ nullptr }; + QOpenGLContextWrapper* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; + ProgramPtr _program; + ShapeWrapperPtr _plane; + QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { + _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - connect(&_timer, &QTimer::timeout, [=] { - draw(); - }); + QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); - DependencyManager::set(); - setSurfaceType(QSurface::OpenGLSurface); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - QSurfaceFormat format; - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); - format.setOption(QSurfaceFormat::DebugContext); + { + setSurfaceType(QSurface::OpenGLSurface); + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + setFormat(format); + _context = new QOpenGLContextWrapper(); + _context->setFormat(format); + _context->setShareContext(_chromiumShareContext->getContext()); + } - setFormat(format); - _context = new QOpenGLContext; - _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); - initializeOpenGLFunctions(); - { - QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); - logger->initialize(); // initializes in the current context, i.e. ctx - logger->enableMessages(); - connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; - }); - // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); - } + glewExperimental = true; + glewInit(); + glGetError(); - qDebug() << (const char*)this->glGetString(GL_VERSION); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glDisable(GL_DEPTH_TEST); + using namespace oglplus; + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); + Context::Disable(Capability::DepthTest); + Context::Disable(Capability::CullFace); + Context::ClearColor(0.2f, 0.2f, 0.2f, 1); MessageDialog::registerType(); - VrMenu::registerType(); InfoView::registerType(); + auto offscreenUi = DependencyManager::set(); + { + offscreenUi->create(_context->getContext()); + offscreenUi->setProxyWindow(this); - auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_context); - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); - makeCurrent(); + makeCurrent(); + } - offscreenUi->setProxyWindow(this); - QDesktopWidget* desktop = QApplication::desktop(); - QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); - int height = rect.height(); - //rect.setHeight(height / 2); - rect.setY(rect.y() + height / 2); + + auto primaryScreen = QGuiApplication::primaryScreen(); + auto targetScreen = primaryScreen; + auto screens = QGuiApplication::screens(); + if (screens.size() > 1) { + for (auto screen : screens) { + if (screen != targetScreen) { + targetScreen = screen; + break; + } + } + } + auto rect = targetScreen->availableGeometry(); + rect.setWidth(rect.width() * 0.8f); + rect.setHeight(rect.height() * 0.8f); + rect.moveTo(QPoint(20, 20)); setGeometry(rect); -// setFramePosition(QPoint(-1000, 0)); -// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); - offscreenUi->load(QUrl("TestMenu.qml")); - // Requires a root menu to have been loaded before it can load - VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -227,16 +529,35 @@ private: } makeCurrent(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); + auto error = glGetError(); + if (error != GL_NO_ERROR) { + qDebug() << "GL error in entering draw " << error; + } - renderQml(); + using namespace oglplus; + Context::Clear().ColorBuffer().DepthBuffer(); + ivec2 size(_size.width(), _size.height()); + size *= devicePixelRatio(); + size = glm::max(size, ivec2(100, 100)); + Context::Viewport(size.x, size.y); + if (!_program) { + _program = loadDefaultShader(); + _plane = loadPlane(_program); + } + if (testQmlTexture > 0) { + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + _program->Bind(); + _plane->Use(); + _plane->Draw(); _context->swapBuffers(this); - glFinish(); fps.increment(); - if (fps.elapsed() >= 2.0f) { + if (fps.elapsed() >= 10.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -246,8 +567,6 @@ private: _context->makeCurrent(this); } - void renderQml(); - void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -269,11 +588,13 @@ protected: offscreenUi->load("Browser.qml"); } break; - case Qt::Key_L: + + case Qt::Key_J: if (event->modifiers() & Qt::CTRL) { - InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); + loadScript(getExamplesDir() + "tests/qmlWebTest.js"); } break; + case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -281,22 +602,9 @@ protected: }); } break; - case Qt::Key_J: - if (event->modifiers() & Qt::CTRL) { - auto offscreenUi = DependencyManager::get(); - rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); - QMetaObject::invokeMethod(rootMenu, "popup"); - } - break; } QWindow::keyPressEvent(event); } - QQmlContext* menuContext{ nullptr }; - void keyReleaseEvent(QKeyEvent *event) override { - if (_altPressed && Qt::Key_Alt == event->key()) { - VrMenu::toggle(); - } - } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -308,40 +616,26 @@ protected: } }; -void QTestWindow::renderQml() { - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - if (testQmlTexture > 0) { - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glBegin(GL_QUADS); - { - glTexCoord2f(0, 0); - glVertex2f(-1, -1); - glTexCoord2f(0, 1); - glVertex2f(-1, 1); - glTexCoord2f(1, 1); - glVertex2f(1, 1); - glTexCoord2f(1, 0); - glVertex2f(1, -1); - } - glEnd(); -} - - const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = message; + +#ifdef Q_OS_WIN + if (!logMessage.isEmpty()) { + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); + } +#endif +} + + int main(int argc, char** argv) { - QApplication app(argc, argv); + QGuiApplication app(argc, argv); + qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From ae6cf131f55c6629347c3cd449bd15dbb7158cf1 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:16:56 -0800 Subject: [PATCH 36/41] Fixing crash bug --- interface/resources/qml/QmlWebWindow.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 9664c0edb8..3eb01aa9ba 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -5,7 +5,6 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import QtWebSockets 1.0 -import "qwebchannel.js" as WebChannel import "controls" import "styles" From e929e3f789f94b4305e3801c1a3fb74755b6d3c0 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 18 Dec 2015 19:18:39 -0800 Subject: [PATCH 37/41] more work on angular velocity tests --- ...t.js => reticleHandAngularVelocityTest.js} | 60 +++++++++++++------ libraries/script-engine/src/Quat.cpp | 4 ++ libraries/script-engine/src/Quat.h | 1 + 3 files changed, 48 insertions(+), 17 deletions(-) rename examples/controllers/{reticleHandTest.js => reticleHandAngularVelocityTest.js} (57%) diff --git a/examples/controllers/reticleHandTest.js b/examples/controllers/reticleHandAngularVelocityTest.js similarity index 57% rename from examples/controllers/reticleHandTest.js rename to examples/controllers/reticleHandAngularVelocityTest.js index 4132c80d85..94288b7bfb 100644 --- a/examples/controllers/reticleHandTest.js +++ b/examples/controllers/reticleHandAngularVelocityTest.js @@ -1,5 +1,5 @@ // -// reticleTest.js +// reticleHandAngularVelocityTest.js // examples/controllers // // Created by Brad Hefta-Gaub on 2015/12/15 @@ -9,6 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +// If you set this to true, you will get the raw instantaneous angular velocity. +// note: there is a LOT of noise in the hydra rotation, you will probably be very +// frustrated with the level of jitter. +var USE_INSTANTANEOUS_ANGULAR_VELOCITY = false; +var whichHand = Controller.Standard.RightHand; +var whichTrigger = Controller.Standard.RT; + + function msecTimestampNow() { var d = new Date(); @@ -30,7 +38,7 @@ function moveReticle(dX, dY) { // some debugging to see if position is jumping around on us... var distanceSinceLastMove = length(lastPos, globalPos); if (distanceSinceLastMove > EXPECTED_CHANGE) { - print("distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); + print("------------------ distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); } if (Math.abs(dX) > EXPECTED_CHANGE) { @@ -48,12 +56,25 @@ function moveReticle(dX, dY) { lastPos = globalPos; } +var firstTime = true; var lastTime = msecTimestampNow(); +var previousRotation; var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand"; var mapping = Controller.newMapping(MAPPING_NAME); -mapping.from(Controller.Standard.RightHand).peek().to(function(pose) { +mapping.from(whichTrigger).peek().constrainToInteger().to(Controller.Actions.ReticleClick); +mapping.from(whichHand).peek().to(function(pose) { + var MSECS_PER_SECOND = 1000; + var now = msecTimestampNow(); + var deltaMsecs = (now - lastTime); + var deltaTime = deltaMsecs / MSECS_PER_SECOND; + + if (firstTime) { + previousRotation = pose.rotation; + lastTime = msecTimestampNow(); + firstTime = false; + } // pose.angularVelocity - is the angularVelocity in a "physics" sense, that // means the direction of the vector is the axis of symetry of rotation @@ -67,29 +88,34 @@ mapping.from(Controller.Standard.RightHand).peek().to(function(pose) { var xPart = -pose.angularVelocity.y; var yPart = -pose.angularVelocity.x; + // pose.angularVelocity is "smoothed", we can calculate our own instantaneous + // angular velocity as such: + if (USE_INSTANTANEOUS_ANGULAR_VELOCITY) { + var previousConjugate = Quat.conjugate(previousRotation); + var deltaRotation = Quat.multiply(pose.rotation, previousConjugate); + var normalizedDeltaRotation = Quat.normalize(deltaRotation); + var axis = Quat.axis(normalizedDeltaRotation); + var speed = Quat.angle(normalizedDeltaRotation) / deltaTime; + var instantaneousAngularVelocity = Vec3.multiply(speed, axis); + + xPart = -instantaneousAngularVelocity.y; + yPart = -instantaneousAngularVelocity.x; + + previousRotation = pose.rotation; + } var MOVE_SCALE = 1; - var MSECS_PER_SECOND = 1000; - var now = msecTimestampNow(); - var secondsSinceLast = (now - lastTime) / MSECS_PER_SECOND; lastTime = now; - //print("secondsSinceLast:" + secondsSinceLast); - - //print("x part:" + xPart); - //print("y part:" + yPart); - - var dX = (xPart * MOVE_SCALE) / secondsSinceLast; - var dY = (yPart * MOVE_SCALE) / secondsSinceLast; - - //print("dX:" + dX); - //print("dY:" + dY); + var dX = (xPart * MOVE_SCALE) / deltaTime; + var dY = (yPart * MOVE_SCALE) / deltaTime; moveReticle(dX, dY); }); mapping.enable(); - Script.scriptEnding.connect(function(){ mapping.disable(); }); + + diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index bb74b20be0..f6b1726770 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -22,6 +22,10 @@ quat Quat::normalize(const glm::quat& q) { return glm::normalize(q); } +quat Quat::conjugate(const glm::quat& q) { + return glm::conjugate(q); +} + glm::quat Quat::rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { return ::rotationBetween(v1, v2); } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 543c93401f..bc7f11ab01 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -26,6 +26,7 @@ class Quat : public QObject { public slots: glm::quat multiply(const glm::quat& q1, const glm::quat& q2); glm::quat normalize(const glm::quat& q); + glm::quat conjugate(const glm::quat& q); glm::quat lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); glm::quat lookAtSimple(const glm::vec3& eye, const glm::vec3& center); glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); From 8b9de716f137706423b8bd29418aea32f80e6a61 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 19 Dec 2015 08:08:10 -0800 Subject: [PATCH 38/41] fix bad pointer juju --- assignment-client/src/octree/OctreeQueryNode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index 044b44a165..edb737ddd5 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -33,7 +33,7 @@ bool OctreeQueryNode::packetIsDuplicate() const { // of the entire packet, we need to compare only the packet content... if (_lastOctreePacketLength == _octreePacket->getPayloadSize()) { - if (memcmp(&_lastOctreePayload + OCTREE_PACKET_EXTRA_HEADERS_SIZE, + if (memcmp(_lastOctreePayload.data() + OCTREE_PACKET_EXTRA_HEADERS_SIZE, _octreePacket->getPayload() + OCTREE_PACKET_EXTRA_HEADERS_SIZE, _octreePacket->getPayloadSize() - OCTREE_PACKET_EXTRA_HEADERS_SIZE) == 0) { return true; From 9271209d8907d01a5365540e46791137efe7dadf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 19 Dec 2015 08:13:53 -0800 Subject: [PATCH 39/41] another --- assignment-client/src/octree/OctreeQueryNode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index edb737ddd5..78a049edd6 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -101,7 +101,7 @@ void OctreeQueryNode::resetOctreePacket() { // scene information, (e.g. the root node packet of a static scene), we can use this as a strategy for reducing // packet send rate. _lastOctreePacketLength = _octreePacket->getPayloadSize(); - memcpy(&_lastOctreePayload, _octreePacket->getPayload(), _lastOctreePacketLength); + memcpy(_lastOctreePayload.data(), _octreePacket->getPayload(), _lastOctreePacketLength); // If we're moving, and the client asked for low res, then we force monochrome, otherwise, use // the clients requested color state. From 248b3d09ace2a887283b255194a429c087e7a981 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Mon, 21 Dec 2015 10:49:49 -0800 Subject: [PATCH 40/41] add absolute rotation based hand reticle movement --- .../controllers/reticleHandRotationTest.js | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 examples/controllers/reticleHandRotationTest.js diff --git a/examples/controllers/reticleHandRotationTest.js b/examples/controllers/reticleHandRotationTest.js new file mode 100644 index 0000000000..d413e531df --- /dev/null +++ b/examples/controllers/reticleHandRotationTest.js @@ -0,0 +1,103 @@ +// +// reticleHandRotationTest.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Math.clamp=function(a,b,c) { + return Math.max(b,Math.min(c,a)); +} + +var whichHand = Controller.Standard.RightHand; +var whichTrigger = Controller.Standard.RT; + +function length(posA, posB) { + var dx = posA.x - posB.x; + var dy = posA.y - posB.y; + var length = Math.sqrt((dx*dx) + (dy*dy)) + return length; +} + +var EXPECTED_CHANGE = 50; +var lastPos = Controller.getReticlePosition(); +function moveReticleAbsolute(x, y) { + var globalPos = Controller.getReticlePosition(); + var dX = x - globalPos.x; + var dY = y - globalPos.y; + + // some debugging to see if position is jumping around on us... + var distanceSinceLastMove = length(lastPos, globalPos); + if (distanceSinceLastMove > EXPECTED_CHANGE) { + print("------------------ distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------"); + } + + if (Math.abs(dX) > EXPECTED_CHANGE) { + print("surpressing unexpectedly large change dX:" + dX + "----------------------------"); + } + if (Math.abs(dY) > EXPECTED_CHANGE) { + print("surpressing unexpectedly large change dY:" + dY + "----------------------------"); + } + + globalPos.x = x; + globalPos.y = y; + Controller.setReticlePosition(globalPos); + lastPos = globalPos; +} + + +var MAPPING_NAME = "com.highfidelity.testing.reticleWithHandRotation"; +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from(whichTrigger).peek().constrainToInteger().to(Controller.Actions.ReticleClick); +mapping.from(whichHand).peek().to(function(pose) { + + // NOTE: hack for now + var screenSizeX = 1920; + var screenSizeY = 1080; + + var rotated = Vec3.multiplyQbyV(pose.rotation, Vec3.UNIT_NEG_Y); // + var absolutePitch = rotated.y; // from 1 down to -1 up ... but note: if you rotate down "too far" it starts to go up again... + var absoluteYaw = rotated.z; // from -1 left to 1 right + //print("absolutePitch:" + absolutePitch); + //print("absoluteYaw:" + absoluteYaw); + //Vec3.print("rotated:", rotated); + + var ROTATION_BOUND = 0.6; + var clampYaw = Math.clamp(absoluteYaw, -ROTATION_BOUND, ROTATION_BOUND); + var clampPitch = Math.clamp(absolutePitch, -ROTATION_BOUND, ROTATION_BOUND); + //var clampYaw = absoluteYaw; + //print("clampYaw:" + clampYaw); + //print("clampPitch:" + clampPitch); + + // if using entire span... + //var xRatio = (absoluteYaw + 1) / 2; + //var yRatio = (absolutePitch + 1) / 2; + + // if using only from -0.5 to 0.5 + var xRatio = (clampYaw + ROTATION_BOUND) / (2 * ROTATION_BOUND); + var yRatio = (clampPitch + ROTATION_BOUND) / (2 * ROTATION_BOUND); + + //print("xRatio:" + xRatio); + //print("yRatio:" + yRatio); + + //print("ratio x:" + xRatio + " y:" + yRatio); + + var x = screenSizeX * xRatio; + var y = screenSizeY * yRatio; + + //print("position x:" + x + " y:" + y); + if (!(xRatio == 0.5 && yRatio == 0)) { + moveReticleAbsolute(x, y); + } +}); +mapping.enable(); + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); + + From 05b7fcc9573dd43a9df57df4d901b618767918b0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 21 Dec 2015 11:19:05 -0800 Subject: [PATCH 41/41] Remove script -> qt::webchannel dependency --- interface/src/Application.cpp | 1 + libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e22b3913d0..f4fe919780 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4183,6 +4183,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri LocationScriptingInterface::locationSetter); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); + scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index c1131765f7..3796abd92a 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5f39bee9fa..ded3db11e9 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,7 +30,6 @@ #include #include -#include #include #include @@ -351,8 +350,6 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); - - registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue);