From d17e04967a7dbbfbd12ce33a08e279f8626b1907 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 18 Jan 2019 13:49:16 -0800 Subject: [PATCH 001/130] fix zone bugs --- .../src/RenderableZoneEntityItem.cpp | 61 +++++++++++-------- libraries/entities/src/ZoneEntityItem.cpp | 3 +- libraries/entities/src/ZoneEntityItem.h | 3 - libraries/graphics/src/graphics/Haze.cpp | 9 --- libraries/graphics/src/graphics/Haze.h | 30 ++++----- libraries/render-utils/src/Haze.slh | 2 +- 6 files changed, 51 insertions(+), 57 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 57ff8ed8c2..631148c27a 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -198,24 +198,33 @@ void ZoneEntityRenderer::removeFromScene(const ScenePointer& scene, Transaction& void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { DependencyManager::get()->updateZone(entity->getID()); + auto position = entity->getWorldPosition(); + auto rotation = entity->getWorldOrientation(); + auto dimensions = entity->getScaledDimensions(); + bool rotationChanged = rotation != _lastRotation; + bool transformChanged = rotationChanged || position != _lastPosition || dimensions != _lastDimensions; + + auto proceduralUserData = entity->getUserData(); + bool proceduralUserDataChanged = _proceduralUserData != proceduralUserData; + // FIXME one of the bools here could become true between being fetched and being reset, // resulting in a lost update - bool keyLightChanged = entity->keyLightPropertiesChanged(); - bool ambientLightChanged = entity->ambientLightPropertiesChanged(); - bool skyboxChanged = entity->skyboxPropertiesChanged(); + bool keyLightChanged = entity->keyLightPropertiesChanged() || rotationChanged; + bool ambientLightChanged = entity->ambientLightPropertiesChanged() || transformChanged; + bool skyboxChanged = entity->skyboxPropertiesChanged() || proceduralUserDataChanged; bool hazeChanged = entity->hazePropertiesChanged(); bool bloomChanged = entity->bloomPropertiesChanged(); - entity->resetRenderingPropertiesChanged(); - _lastPosition = entity->getWorldPosition(); - _lastRotation = entity->getWorldOrientation(); - _lastDimensions = entity->getScaledDimensions(); - _keyLightProperties = entity->getKeyLightProperties(); - _ambientLightProperties = entity->getAmbientLightProperties(); - _skyboxProperties = entity->getSkyboxProperties(); - _hazeProperties = entity->getHazeProperties(); - _bloomProperties = entity->getBloomProperties(); + if (transformChanged) { + _lastPosition = entity->getWorldPosition(); + _lastRotation = entity->getWorldOrientation(); + _lastDimensions = entity->getScaledDimensions(); + } + + if (proceduralUserDataChanged) { + _proceduralUserData = entity->getUserData(); + } #if 0 if (_lastShapeURL != _typedEntity->getCompoundShapeURL()) { @@ -239,21 +248,29 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen updateKeyZoneItemFromEntity(entity); if (keyLightChanged) { + _keyLightProperties = entity->getKeyLightProperties(); updateKeySunFromEntity(entity); } if (ambientLightChanged) { + _ambientLightProperties = entity->getAmbientLightProperties(); updateAmbientLightFromEntity(entity); } - if (skyboxChanged || _proceduralUserData != entity->getUserData()) { + if (skyboxChanged) { + _skyboxProperties = entity->getSkyboxProperties(); updateKeyBackgroundFromEntity(entity); } if (hazeChanged) { + _hazeProperties = entity->getHazeProperties(); updateHazeFromEntity(entity); } + if (bloomChanged) { + _bloomProperties = entity->getBloomProperties(); + updateBloomFromEntity(entity); + } bool visuallyReady = true; uint32_t skyboxMode = entity->getSkyboxMode(); @@ -264,10 +281,6 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen } entity->setVisuallyReady(visuallyReady); - - if (bloomChanged) { - updateBloomFromEntity(entity); - } } void ZoneEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { @@ -344,7 +357,7 @@ void ZoneEntityRenderer::updateKeySunFromEntity(const TypedEntityPointer& entity // Set the keylight sunLight->setColor(ColorUtils::toVec3(_keyLightProperties.getColor())); sunLight->setIntensity(_keyLightProperties.getIntensity()); - sunLight->setDirection(entity->getTransform().getRotation() * _keyLightProperties.getDirection()); + sunLight->setDirection(_lastRotation * _keyLightProperties.getDirection()); sunLight->setCastShadows(_keyLightProperties.getCastShadows()); } @@ -356,7 +369,6 @@ void ZoneEntityRenderer::updateAmbientLightFromEntity(const TypedEntityPointer& ambientLight->setPosition(_lastPosition); ambientLight->setOrientation(_lastRotation); - // Set the ambient light ambientLight->setAmbientIntensity(_ambientLightProperties.getAmbientIntensity()); @@ -395,8 +407,6 @@ void ZoneEntityRenderer::updateHazeFromEntity(const TypedEntityPointer& entity) haze->setHazeAttenuateKeyLight(_hazeProperties.getHazeAttenuateKeyLight()); haze->setHazeKeyLightRangeFactor(graphics::Haze::convertHazeRangeToHazeRangeFactor(_hazeProperties.getHazeKeyLightRange())); haze->setHazeKeyLightAltitudeFactor(graphics::Haze::convertHazeAltitudeToHazeAltitudeFactor(_hazeProperties.getHazeKeyLightAltitude())); - - haze->setTransform(entity->getTransform().getMatrix()); } void ZoneEntityRenderer::updateBloomFromEntity(const TypedEntityPointer& entity) { @@ -414,13 +424,13 @@ void ZoneEntityRenderer::updateKeyBackgroundFromEntity(const TypedEntityPointer& editBackground(); setSkyboxColor(toGlm(_skyboxProperties.getColor())); - setProceduralUserData(entity->getUserData()); + setProceduralUserData(_proceduralUserData); setSkyboxURL(_skyboxProperties.getURL()); } void ZoneEntityRenderer::updateKeyZoneItemFromEntity(const TypedEntityPointer& entity) { // Update rotation values - editSkybox()->setOrientation(entity->getTransform().getRotation()); + editSkybox()->setOrientation(_lastRotation); /* TODO: Implement the sun model behavior / Keep this code here for reference, this is how we { @@ -540,9 +550,6 @@ void ZoneEntityRenderer::setSkyboxColor(const glm::vec3& color) { } void ZoneEntityRenderer::setProceduralUserData(const QString& userData) { - if (_proceduralUserData != userData) { - _proceduralUserData = userData; - std::dynamic_pointer_cast(editSkybox())->parse(_proceduralUserData); - } + std::dynamic_pointer_cast(editSkybox())->parse(userData); } diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 7f7f6170d4..7b0491dbc0 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -119,7 +119,7 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode); somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged || - _stagePropertiesChanged || _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged; + _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged; return somethingChanged; } @@ -394,7 +394,6 @@ void ZoneEntityItem::resetRenderingPropertiesChanged() { _skyboxPropertiesChanged = false; _hazePropertiesChanged = false; _bloomPropertiesChanged = false; - _stagePropertiesChanged = false; }); } diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 813115add9..11c85dab89 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -102,8 +102,6 @@ public: bool hazePropertiesChanged() const { return _hazePropertiesChanged; } bool bloomPropertiesChanged() const { return _bloomPropertiesChanged; } - bool stagePropertiesChanged() const { return _stagePropertiesChanged; } - void resetRenderingPropertiesChanged(); virtual bool supportsDetailedIntersection() const override { return true; } @@ -155,7 +153,6 @@ protected: bool _skyboxPropertiesChanged { false }; bool _hazePropertiesChanged{ false }; bool _bloomPropertiesChanged { false }; - bool _stagePropertiesChanged { false }; static bool _drawZoneBoundaries; static bool _zonesArePickable; diff --git a/libraries/graphics/src/graphics/Haze.cpp b/libraries/graphics/src/graphics/Haze.cpp index ded48429ba..d9bee7507f 100644 --- a/libraries/graphics/src/graphics/Haze.cpp +++ b/libraries/graphics/src/graphics/Haze.cpp @@ -182,12 +182,3 @@ void Haze::setHazeBackgroundBlend(const float hazeBackgroundBlend) { _hazeParametersBuffer.edit().hazeBackgroundBlend = newBlend; } } - -void Haze::setTransform(const glm::mat4& transform) { - auto& params = _hazeParametersBuffer.get(); - - if (params.transform != transform) { - _hazeParametersBuffer.edit().transform = transform; - } -} - diff --git a/libraries/graphics/src/graphics/Haze.h b/libraries/graphics/src/graphics/Haze.h index 59138319f4..25004f098f 100644 --- a/libraries/graphics/src/graphics/Haze.h +++ b/libraries/graphics/src/graphics/Haze.h @@ -92,8 +92,6 @@ namespace graphics { void setHazeBackgroundBlend(const float hazeBackgroundBlend); - void setTransform(const glm::mat4& transform); - using UniformBufferView = gpu::BufferView; UniformBufferView getHazeParametersBuffer() const { return _hazeParametersBuffer; } @@ -101,30 +99,32 @@ namespace graphics { class Parameters { public: // DO NOT CHANGE ORDER HERE WITHOUT UNDERSTANDING THE std140 LAYOUT - glm::vec3 hazeColor{ INITIAL_HAZE_COLOR }; - float hazeGlareBlend{ convertGlareAngleToPower(INITIAL_HAZE_GLARE_ANGLE) }; + glm::vec3 hazeColor { INITIAL_HAZE_COLOR }; + float hazeGlareBlend { convertGlareAngleToPower(INITIAL_HAZE_GLARE_ANGLE) }; - glm::vec3 hazeGlareColor{ INITIAL_HAZE_GLARE_COLOR }; - float hazeBaseReference{ INITIAL_HAZE_BASE_REFERENCE }; + glm::vec3 hazeGlareColor { INITIAL_HAZE_GLARE_COLOR }; + float hazeBaseReference { INITIAL_HAZE_BASE_REFERENCE }; glm::vec3 colorModulationFactor; - int hazeMode{ 0 }; // bit 0 - set to activate haze attenuation of fragment color + int hazeMode { 0 }; // bit 0 - set to activate haze attenuation of fragment color // bit 1 - set to add the effect of altitude to the haze attenuation // bit 2 - set to activate directional light attenuation mode // bit 3 - set to blend between blend-in and blend-out colours - glm::mat4 transform; + // Padding required to align the struct +#if defined(__clang__) + __attribute__((unused)) +#endif + vec3 __padding; // Amount of background (skybox) to display, overriding the haze effect for the background - float hazeBackgroundBlend{ INITIAL_HAZE_BACKGROUND_BLEND }; + float hazeBackgroundBlend { INITIAL_HAZE_BACKGROUND_BLEND }; // The haze attenuation exponents used by both fragment and directional light attenuation - float hazeRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_HAZE_RANGE) }; - float hazeHeightFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_HAZE_HEIGHT) }; - float hazeKeyLightRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_KEY_LIGHT_RANGE) }; + float hazeRangeFactor { convertHazeRangeToHazeRangeFactor(INITIAL_HAZE_RANGE) }; + float hazeHeightFactor { convertHazeAltitudeToHazeAltitudeFactor(INITIAL_HAZE_HEIGHT) }; + float hazeKeyLightRangeFactor { convertHazeRangeToHazeRangeFactor(INITIAL_KEY_LIGHT_RANGE) }; - float hazeKeyLightAltitudeFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_KEY_LIGHT_ALTITUDE) }; - // Padding required to align the structure to sizeof(vec4) - vec3 __padding; + float hazeKeyLightAltitudeFactor { convertHazeAltitudeToHazeAltitudeFactor(INITIAL_KEY_LIGHT_ALTITUDE) }; Parameters() {} }; diff --git a/libraries/render-utils/src/Haze.slh b/libraries/render-utils/src/Haze.slh index 0bf1d5d689..e2285febe4 100644 --- a/libraries/render-utils/src/Haze.slh +++ b/libraries/render-utils/src/Haze.slh @@ -28,7 +28,7 @@ struct HazeParams { vec3 colorModulationFactor; int hazeMode; - mat4 transform; + vec3 spare; float backgroundBlend; float hazeRangeFactor; From 618146e885c2e08e16040ffe7d303991b9b55cee Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 18 Jan 2019 15:36:10 -0800 Subject: [PATCH 002/130] fix animation url change --- .../src/RenderableModelEntityItem.cpp | 35 +++++++------------ .../src/RenderableModelEntityItem.h | 9 ++--- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index aa449b8919..b8dc7816f1 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -959,23 +959,6 @@ QStringList RenderableModelEntityItem::getJointNames() const { return result; } -void RenderableModelEntityItem::setAnimationURL(const QString& url) { - QString oldURL = getAnimationURL(); - ModelEntityItem::setAnimationURL(url); - if (oldURL != getAnimationURL()) { - _needsAnimationReset = true; - } -} - -bool RenderableModelEntityItem::needsAnimationReset() const { - return _needsAnimationReset; -} - -QString RenderableModelEntityItem::getAnimationURLAndReset() { - _needsAnimationReset = false; - return getAnimationURL(); -} - scriptable::ScriptableModelBase render::entities::ModelEntityRenderer::getScriptableModel() { auto model = resultWithReadLock([this]{ return _model; }); @@ -1474,11 +1457,17 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce if (_animating) { DETAILED_PROFILE_RANGE(simulation_physics, "Animate"); - if (_animation && entity->needsAnimationReset()) { - //(_animation->getURL().toString() != entity->getAnimationURL())) { // bad check - // the joints have been mapped before but we have a new animation to load - _animation.reset(); - _jointMappingCompleted = false; + auto animationURL = entity->getAnimationURL(); + bool animationChanged = _animationURL != animationURL; + if (animationChanged) { + _animationURL = animationURL; + + if (_animation) { + //(_animation->getURL().toString() != entity->getAnimationURL())) { // bad check + // the joints have been mapped before but we have a new animation to load + _animation.reset(); + _jointMappingCompleted = false; + } } if (!_jointMappingCompleted) { @@ -1525,7 +1514,7 @@ void ModelEntityRenderer::mapJoints(const TypedEntityPointer& entity, const Mode } if (!_animation) { - _animation = DependencyManager::get()->getAnimation(entity->getAnimationURLAndReset()); + _animation = DependencyManager::get()->getAnimation(_animationURL); } if (_animation && _animation->isLoaded()) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 725c1d96c3..c3a7a49272 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -113,10 +113,6 @@ public: virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; - void setAnimationURL(const QString& url) override; - bool needsAnimationReset() const; - QString getAnimationURLAndReset(); - private: bool needsUpdateModelBounds() const; void autoResizeJointArrays(); @@ -131,7 +127,6 @@ private: bool _originalTexturesRead { false }; bool _dimensionsInitialized { true }; bool _needsJointSimulation { false }; - bool _needsAnimationReset { false }; }; namespace render { namespace entities { @@ -188,12 +183,12 @@ private: const void* _collisionMeshKey { nullptr }; - // used on client side + QUrl _parsedModelURL; bool _jointMappingCompleted { false }; QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints AnimationPointer _animation; - QUrl _parsedModelURL; bool _animating { false }; + QString _animationURL; uint64_t _lastAnimated { 0 }; render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; From 8010d86210bef1c2d95c235bba9b112d0e3a2e1a Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 21 Jan 2019 13:39:16 -0800 Subject: [PATCH 003/130] QML Marketplace support Support QML UI for the Marketplace as some devices do not handle web on 3d surfaces. Checkpoint code --- .../hifi/commerce/marketplace/Marketplace.qml | 336 ++++++++++++++++++ interface/src/Application.cpp | 9 + interface/src/commerce/QmlMarketplace.cpp | 131 +++++++ interface/src/commerce/QmlMarketplace.h | 68 ++++ scripts/system/marketplaces/marketplace.js | 117 +----- scripts/system/marketplaces/marketplaces.js | 8 +- 6 files changed, 565 insertions(+), 104 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml create mode 100644 interface/src/commerce/QmlMarketplace.cpp create mode 100644 interface/src/commerce/QmlMarketplace.h diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml new file mode 100644 index 0000000000..fb8b410e3f --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -0,0 +1,336 @@ +// +// Marketplace.qml +// qml/hifi/commerce/marketplace +// +// Marketplace +// +// Created by Roxanne Skelly on 2019-01-18 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + + property string activeView: "initialize"; + property bool keyboardRaised: false; + property int category_index: -1; + property alias categoryChoices: categoriesModel; + + anchors.fill: (typeof parent === undefined) ? undefined : parent; + + Component.onDestruction: { + KeyboardScriptingInterface.raised = false; + } + + Connections { + target: Marketplace; + + onGetMarketplaceCategoriesResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Categories", result.data.message); + } else { + + } + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + } + + // + // HEADER BAR START + // + Item { + id: header; + visible: true; + width: parent.width; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + + Item { + id: titleBarContainer; + visible: true; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Wallet icon + Image { + id: walletIcon; + source: "../../../../images/hifi-logo-blackish.svg"; + height: 20 + width: walletIcon.height; + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + visible: true; + } + + // Title Bar text + RalewaySemiBold { + id: titleBarText; + text: "Marketplace"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: walletIcon.right; + anchors.leftMargin: 6; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.black; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + } + + Item { + id: searchBarContainer; + visible: true; + // Size + width: parent.width; + anchors.top: titleBarContainer.bottom; + height: 50; + + + Rectangle { + id: categoriesButton; + anchors.left: parent.left; + anchors.leftMargin: 10; + anchors.verticalCenter: parent.verticalCenter; + height: 34; + width: categoriesText.width + 25; + color: "white"; + radius: 4; + border.width: 1; + border.color: hifi.colors.lightGray; + + + // Categories Text + RalewayRegular { + id: categoriesText; + text: "Categories"; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + elide: Text.ElideRight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + width: Math.min(textMetrics.width + 25, 110); + // Anchors + anchors.centerIn: parent; + rightPadding: 10; + } + HiFiGlyphs { + id: categoriesDropdownIcon; + text: hifi.glyphs.caratDn; + // Size + size: 34; + // Anchors + anchors.right: parent.right; + anchors.rightMargin: -8; + anchors.verticalCenter: parent.verticalCenter; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.baseGray; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + categoriesDropdown.visible = !categoriesDropdown.visible; + } + onEntered: categoriesText.color = hifi.colors.baseGrayShadow; + onExited: categoriesText.color = hifi.colors.baseGray; + } + + Component.onCompleted: { + console.log("Getting Marketplace Categories"); + console.log(JSON.stringify(Marketplace)); + Marketplace.getMarketplaceItems(); + } + } + + Rectangle { + id: categoriesContainer; + visible: true; + height: 50 * categoriesModel.count; + width: parent.width; + anchors.top: categoriesButton.bottom; + anchors.left: categoriesButton.left; + color: hifi.colors.white; + + ListModel { + id: categoriesModel; + } + + ListView { + id: dropdownListView; + interactive: false; + anchors.fill: parent; + model: categoriesModel; + delegate: Item { + width: parent.width; + height: 50; + Rectangle { + id: dropDownButton; + color: hifi.colors.white; + width: parent.width; + height: 50; + visible: true; + + RalewaySemiBold { + id: dropDownButtonText; + text: model.displayName; + anchors.fill: parent; + anchors.topMargin: 2; + anchors.leftMargin: 12; + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + onEntered: { + dropDownButton.color = hifi.colors.blueHighlight; + } + onExited: { + dropDownButton.color = hifi.colors.white; + } + onClicked: { + root.category_index = index; + dropdownContainer.visible = false; + } + } + } + Rectangle { + height: 2; + width: parent.width; + color: hifi.colors.lightGray; + visible: model.separator + } + } + } + } + + // or + RalewayRegular { + id: orText; + text: "or"; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + elide: Text.ElideRight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + width: Math.min(textMetrics.width + 25, 110); + // Anchors + anchors.left: categoriesButton.right; + rightPadding: 10; + leftPadding: 10; + anchors.verticalCenter: parent.verticalCenter; + } + HifiControlsUit.TextField { + id: searchField; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.left: orText.right; + anchors.rightMargin: 10; + height: 34; + isSearchField: true; + colorScheme: hifi.colorSchemes.faintGray; + + + font.family: "Fira Sans" + font.pixelSize: hifi.fontSizes.textFieldInput; + + placeholderText: "Search Marketplace"; + + TextMetrics { + id: primaryFilterTextMetrics; + font.family: "FiraSans Regular"; + font.pixelSize: hifi.fontSizes.textFieldInput; + font.capitalization: Font.AllUppercase; + text: root.primaryFilter_displayName; + } + + // workaround for https://bugreports.qt.io/browse/QTBUG-49297 + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Return: + case Qt.Key_Enter: + event.accepted = true; + + // emit accepted signal manually + if (acceptableInput) { + root.accepted(); + root.forceActiveFocus(); + } + break; + case Qt.Key_Backspace: + if (textField.text === "") { + primaryFilter_index = -1; + } + break; + } + } + + onAccepted: { + root.forceActiveFocus(); + } + + onActiveFocusChanged: { + if (!activeFocus) { + dropdownContainer.visible = false; + } + } + + } + } + } + // + // HEADER BAR END + // + DropShadow { + anchors.fill: header; + horizontalOffset: 0; + verticalOffset: 4; + radius: 4.0; + samples: 9 + color: Qt.rgba(0, 0, 0, 0.25); + source: header; + visible: header.visible; + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7ed05611ee..1ec9c93e12 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -232,6 +232,7 @@ #include "commerce/Ledger.h" #include "commerce/Wallet.h" #include "commerce/QmlCommerce.h" +#include "commerce/QmlMarketplace.h" #include "ResourceRequestObserver.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" @@ -2913,6 +2914,14 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, }, commerceCallback); + + QmlContextCallback marketplaceCallback = [](QQmlContext* context) { + context->setContextProperty("Marketplace", new QmlMarketplace()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, marketplaceCallback); + QmlContextCallback ttsCallback = [](QQmlContext* context) { context->setContextProperty("TextToSpeech", DependencyManager::get().data()); }; diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp new file mode 100644 index 0000000000..99d3bb1ae6 --- /dev/null +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -0,0 +1,131 @@ +// +// QmlMarketplace.cpp +// interface/src/commerce +// +// Guard for safe use of Marketplace by authorized QML. +// +// Created by Roxanne Skelly on 1/18/19. +// Copyright 2019 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 "QmlMarketplace.h" +#include "CommerceLogging.h" +#include "Application.h" +#include "DependencyManager.h" +#include +#include +#include +#include +#include "scripting/HMDScriptingInterface.h" + +#define ApiHandler(NAME) void QmlMarketplace::NAME##Success(QNetworkReply* reply) { emit NAME##Result(apiResponse(#NAME, reply)); } +#define FailHandler(NAME) void QmlMarketplace::NAME##Failure(QNetworkReply* reply) { emit NAME##Result(failResponse(#NAME, reply)); } +#define Handler(NAME) ApiHandler(NAME) FailHandler(NAME) +Handler(getMarketplaceItems) +Handler(getMarketplaceItem) +Handler(marketplaceItemLike) +Handler(getMarketplaceCategories) + +QmlMarketplace::QmlMarketplace() { +} + +void QmlMarketplace::openMarketplace(const QString& marketplaceItemId) { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource("hifi/commerce/marketplace/Marketplace.qml"); + DependencyManager::get()->openTablet(); + if (!marketplaceItemId.isEmpty()) { + tablet->sendToQml(QVariantMap({ { "method", "marketplace_openItem" }, { "itemId", marketplaceItemId } })); + } +} + +void QmlMarketplace::getMarketplaceItems( + const QString& q, + const QString& view, + const QString& category, + const QString& adminFilter, + const QString& adminFilterCost, + const QString& sort, + const bool isFree, + const int& page, + const int& perPage) { + + QString endpoint = "items"; + QJsonObject request; + request["q"] = q; + request["view"] = view; + request["category"] = category; + request["adminFilter"] = adminFilter; + request["adminFilterCost"] = adminFilterCost; + request["sort"] = sort; + request["isFree"] = isFree; + request["page"] = page; + request["perPage"] = perPage; + send(endpoint, "getMarketplaceItemsSuccess", "getMarketplaceItemsFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); +} + +void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) { + QString endpoint = QString("items/") + marketplaceItemId; + QJsonObject request; + send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); +} + +void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) { + QString endpoint = QString("items/") + marketplaceItemId + "/like"; + QJsonObject request; + send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PutOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); +} + +void QmlMarketplace::getMarketplaceCategories() { + QString endpoint = "categories"; + QJsonObject request; + send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request); +} + + +void QmlMarketplace::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) { + auto accountManager = DependencyManager::get(); + const QString URL = "/api/v1/marketplace/"; + JSONCallbackParameters callbackParams(this, success, fail); +#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. + qCInfo(commerce) << "Sending" << QJsonDocument(request).toJson(QJsonDocument::Compact); +#endif + accountManager->sendRequest(URL + endpoint, + authType, + method, + callbackParams, + QJsonDocument(request).toJson()); +} + +QJsonObject QmlMarketplace::apiResponse(const QString& label, QNetworkReply* reply) { + QByteArray response = reply->readAll(); + QJsonObject data = QJsonDocument::fromJson(response).object(); +#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. + qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact); +#endif + return data; +} + +// Non-200 responses are not json: +QJsonObject QmlMarketplace::failResponse(const QString& label, QNetworkReply* reply) { + QString response = reply->readAll(); + qWarning(commerce) << "FAILED" << label << response; + + // tempResult will be NULL if the response isn't valid JSON. + QJsonDocument tempResult = QJsonDocument::fromJson(response.toLocal8Bit()); + if (tempResult.isNull()) { + QJsonObject result + { + { "status", "fail" }, + { "message", response } + }; + return result; + } + else { + return tempResult.object(); + } +} \ No newline at end of file diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h new file mode 100644 index 0000000000..95a1aa3911 --- /dev/null +++ b/interface/src/commerce/QmlMarketplace.h @@ -0,0 +1,68 @@ +// +// QmlMarketplace.h +// interface/src/commerce +// +// Guard for safe use of Marketplace by authorized QML. +// +// Created by Roxanne Skelly on 1/18/19. +// Copyright 2019 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 +// + +#pragma once +#ifndef hifi_QmlMarketplace_h +#define hifi_QmlMarketplace_h + +#include + +#include +#include +#include "AccountManager.h" + +class QmlMarketplace : public QObject { + Q_OBJECT + +public: + QmlMarketplace(); + +public slots: + void getMarketplaceItemsSuccess(QNetworkReply* reply); + void getMarketplaceItemsFailure(QNetworkReply* reply); + void getMarketplaceItemSuccess(QNetworkReply* reply); + void getMarketplaceItemFailure(QNetworkReply* reply); + void getMarketplaceCategoriesSuccess(QNetworkReply* reply); + void getMarketplaceCategoriesFailure(QNetworkReply* reply); + void marketplaceItemLikeSuccess(QNetworkReply* reply); + void marketplaceItemLikeFailure(QNetworkReply* reply); + +protected: + Q_INVOKABLE void openMarketplace(const QString& marketplaceItemId = QString()); + Q_INVOKABLE void getMarketplaceItems( + const QString& q = QString(), + const QString& view = QString(), + const QString& category = QString(), + const QString& adminFilter = QString("published"), + const QString& adminFilterCost = QString(), + const QString& sort = QString(), + const bool isFree = false, + const int& page = 1, + const int& perPage = 20); + Q_INVOKABLE void getMarketplaceItem(const QString& marketplaceItemId); + Q_INVOKABLE void marketplaceItemLike(const QString& marketplaceItemId, const bool like = true); + Q_INVOKABLE void getMarketplaceCategories(); + +signals: + void getMarketplaceItemsResult(QJsonObject result); + void getMarketplaceItemResult(QJsonObject result); + void getMarketplaceCategoriesResult(QJsonObject result); + void marketplaceItemLikeResult(QJsonObject result); + +private: + void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request); + QJsonObject apiResponse(const QString& label, QNetworkReply* reply); + QJsonObject failResponse(const QString& label, QNetworkReply* reply); +}; + +#endif // hifi_QmlMarketplace_h diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index d3e5c96739..70680acd1d 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -1,8 +1,8 @@ // // marketplace.js // -// Created by Eric Levin on 8 Jan 2016 -// Copyright 2016 High Fidelity, Inc. +// Created by Roxanne Skelly on 1/18/2019 +// Copyright 2019 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 @@ -10,108 +10,27 @@ (function() { // BEGIN LOCAL_SCOPE -/* global WebTablet */ -Script.include("../libraries/WebTablet.js"); +var AppUi = Script.require('appUi'); -var toolIconUrl = Script.resolvePath("../assets/images/tools/"); +var BUTTON_NAME = "MARKET"; +var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml"; +var ui; +function startup() { -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; -var marketplaceWindow = new OverlayWebWindow({ - title: "Marketplace", - source: "about:blank", - width: 900, - height: 700, - visible: false -}); - -var toolHeight = 50; -var toolWidth = 50; -var TOOLBAR_MARGIN_Y = 0; -var marketplaceVisible = false; -var marketplaceWebTablet; - -// We persist avatarEntity data in the .ini file, and reconsistitute it on restart. -// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change. -var persistenceKey = "io.highfidelity.lastDomainTablet"; - -function shouldShowWebTablet() { - var rightPose = Controller.getPoseValue(Controller.Standard.RightHand); - var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand); - var hasHydra = !!Controller.Hardware.Hydra; - return HMD.active && (leftPose.valid || rightPose.valid || hasHydra); + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 10, + home: MARKETPLACE_QML_SOURCE + }); } -function showMarketplace(marketplaceID) { - var url = MARKETPLACE_URL; - if (marketplaceID) { - url = url + "/items/" + marketplaceID; - } - tablet.gotoWebScreen(url); - marketplaceVisible = true; - UserActivityLogger.openedMarketplace(); +function shutdown() { } -function hideTablet(tablet) { - if (!tablet) { - return; - } - updateButtonState(false); - tablet.unregister(); - tablet.destroy(); - marketplaceWebTablet = null; - Settings.setValue(persistenceKey, ""); -} -function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated - -} -function hideMarketplace() { - if (marketplaceWindow.visible) { - marketplaceWindow.setVisible(false); - marketplaceWindow.setURL("about:blank"); - } else if (marketplaceWebTablet) { - - } - marketplaceVisible = false; -} - -function toggleMarketplace() { - if (marketplaceVisible) { - hideMarketplace(); - } else { - showMarketplace(); - } -} - -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - -var browseExamplesButton = tablet.addButton({ - icon: "icons/tablet-icons/market-i.svg", - text: "MARKET" -}); - -function updateButtonState(visible) { - -} -function onMarketplaceWindowVisibilityChanged() { - updateButtonState(marketplaceWindow.visible); - marketplaceVisible = marketplaceWindow.visible; -} - -function onClick() { - toggleMarketplace(); -} - -browseExamplesButton.clicked.connect(onClick); -marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged); - -clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash. -// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}), -// but the HUD version stays around, so lets do the same. - -Script.scriptEnding.connect(function () { - browseExamplesButton.clicked.disconnect(onClick); - tablet.removeButton(browseExamplesButton); - marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged); -}); +// +// Run the functions. +// +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e4891a9bae..db3b2e2107 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -769,16 +769,14 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var BUTTON_NAME = "MARKET"; -var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; -// Append "?" if necessary to signal injected script that it's the initial page. -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?"); +var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml"; + var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 9, - inject: MARKETPLACES_INJECT_SCRIPT_URL, - home: MARKETPLACE_URL_INITIAL, + home: MARKETPLACE_QML_SOURCE, onScreenChanged: onTabletScreenChanged, onMessage: onQmlMessageReceived }); From c1ff51a02d5816d1094e98dfdb4692e474b53556 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 24 Jan 2019 17:20:42 -0800 Subject: [PATCH 004/130] QML Marketplace Checkpoint full functionality --- .../hifi/commerce/marketplace/Marketplace.qml | 775 +++++++++++++++--- .../commerce/marketplace/MarketplaceItem.qml | 473 +++++++++++ .../marketplace/MarketplaceListItem.qml | 278 +++++++ .../hifi/commerce/marketplace/SortButton.qml | 87 ++ interface/src/Application.cpp | 2 +- interface/src/commerce/QmlMarketplace.cpp | 42 +- interface/src/commerce/QmlMarketplace.h | 2 +- libraries/networking/src/AccountManager.cpp | 8 +- libraries/networking/src/AccountManager.h | 2 +- scripts/system/commerce/wallet.js | 23 +- scripts/system/html/js/marketplacesInject.js | 13 +- scripts/system/marketplaces/marketplaces.js | 47 +- 12 files changed, 1592 insertions(+), 160 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml create mode 100644 interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml create mode 100644 interface/resources/qml/hifi/commerce/marketplace/SortButton.qml diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index fb8b410e3f..e5ce431de8 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -30,24 +30,72 @@ Rectangle { property string activeView: "initialize"; property bool keyboardRaised: false; - property int category_index: -1; - property alias categoryChoices: categoriesModel; + property int currentSortIndex: 0; + property string sortString: ""; + property string categoryString: ""; + property string searchString: ""; anchors.fill: (typeof parent === undefined) ? undefined : parent; + function getMarketplaceItems() { + marketplaceItemView.visible = false; + itemsList.visible = true; + marketBrowseModel.getFirstPage(); + } + Component.onDestruction: { KeyboardScriptingInterface.raised = false; } Connections { - target: Marketplace; + target: MarketplaceScriptingInterface; onGetMarketplaceCategoriesResult: { if (result.status !== 'success') { console.log("Failed to get Marketplace Categories", result.data.message); } else { - - } + categoriesModel.clear(); + categoriesModel.append({ + id: -1, + name: "Everything" + }); + result.data.items.forEach(function(category) { + categoriesModel.append({ + id: category.id, + name: category.name + }); + }); + } + getMarketplaceItems(); + } + onGetMarketplaceItemsResult: { + marketBrowseModel.handlePage(result.status !== "success" && result.message, result); + } + + onGetMarketplaceItemResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Item", result.data.message); + } else { + + console.log(JSON.stringify(result.data)); + marketplaceItem.item_id = result.data.id; + marketplaceItem.image_url = result.data.thumbnail_url; + marketplaceItem.name = result.data.title; + marketplaceItem.likes = result.data.likes; + marketplaceItem.liked = result.data.has_liked; + marketplaceItem.creator = result.data.creator; + marketplaceItem.categories = result.data.categories; + marketplaceItem.price = result.data.cost; + marketplaceItem.description = result.data.description; + marketplaceItem.attributions = result.data.attributions; + marketplaceItem.license = result.data.license; + marketplaceItem.available = result.data.availability == "available"; + marketplaceItem.created_at = result.data.created_at; + console.log("HEIGHT: " + marketplaceItemContent.height); + marketplaceItemScrollView.contentHeight = marketplaceItemContent.height; + itemsList.visible = false; + marketplaceItemView.visible = true; + } } } @@ -60,15 +108,36 @@ Rectangle { // // HEADER BAR START // - Item { + + + Rectangle { + id: headerShadowBase; + anchors.fill: header; + color: "white"; + } + DropShadow { + anchors.fill: headerShadowBase; + source: headerShadowBase; + verticalOffset: 4; + horizontalOffset: 4; + radius: 6; + samples: 9; + color: hifi.colors.baseGrayShadow; + z:5; + } + Rectangle { id: header; visible: true; - width: parent.width; anchors.left: parent.left; anchors.top: parent.top; anchors.right: parent.right; - - Item { + anchors.topMargin: -1; + anchors.leftMargin: -1; + anchors.rightMargin: -1; + height: childrenRect.height+5; + z: 5; + + Rectangle { id: titleBarContainer; visible: true; // Size @@ -77,13 +146,14 @@ Rectangle { // Anchors anchors.left: parent.left; anchors.top: parent.top; - - // Wallet icon + + + // Marketplace icon Image { - id: walletIcon; + id: marketplaceIcon; source: "../../../../images/hifi-logo-blackish.svg"; height: 20 - width: walletIcon.height; + width: marketplaceIcon.height; anchors.left: parent.left; anchors.leftMargin: 8; anchors.verticalCenter: parent.verticalCenter; @@ -98,7 +168,7 @@ Rectangle { size: hifi.fontSizes.overlayTitle; // Anchors anchors.top: parent.top; - anchors.left: walletIcon.right; + anchors.left: marketplaceIcon.right; anchors.leftMargin: 6; anchors.bottom: parent.bottom; width: paintedWidth; @@ -109,9 +179,10 @@ Rectangle { } } - Item { + Rectangle { id: searchBarContainer; visible: true; + clip: false; // Size width: parent.width; anchors.top: titleBarContainer.bottom; @@ -125,10 +196,10 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter; height: 34; width: categoriesText.width + 25; - color: "white"; + color: hifi.colors.white; radius: 4; border.width: 1; - border.color: hifi.colors.lightGray; + border.color: hifi.colors.lightGrayText; // Categories Text @@ -136,9 +207,9 @@ Rectangle { id: categoriesText; text: "Categories"; // Text size - size: 18; + size: 14; // Style - color: hifi.colors.baseGray; + color: hifi.colors.lightGrayText; elide: Text.ElideRight; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; @@ -166,6 +237,8 @@ Rectangle { hoverEnabled: enabled; onClicked: { categoriesDropdown.visible = !categoriesDropdown.visible; + categoriesButton.color = categoriesDropdown.visible ? hifi.colors.lightGray : hifi.colors.white; + categoriesDropdown.forceActiveFocus(); } onEntered: categoriesText.color = hifi.colors.baseGrayShadow; onExited: categoriesText.color = hifi.colors.baseGray; @@ -173,77 +246,10 @@ Rectangle { Component.onCompleted: { console.log("Getting Marketplace Categories"); - console.log(JSON.stringify(Marketplace)); - Marketplace.getMarketplaceItems(); + MarketplaceScriptingInterface.getMarketplaceCategories(); } } - - Rectangle { - id: categoriesContainer; - visible: true; - height: 50 * categoriesModel.count; - width: parent.width; - anchors.top: categoriesButton.bottom; - anchors.left: categoriesButton.left; - color: hifi.colors.white; - - ListModel { - id: categoriesModel; - } - - ListView { - id: dropdownListView; - interactive: false; - anchors.fill: parent; - model: categoriesModel; - delegate: Item { - width: parent.width; - height: 50; - Rectangle { - id: dropDownButton; - color: hifi.colors.white; - width: parent.width; - height: 50; - visible: true; - - RalewaySemiBold { - id: dropDownButtonText; - text: model.displayName; - anchors.fill: parent; - anchors.topMargin: 2; - anchors.leftMargin: 12; - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 18; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - propagateComposedEvents: false; - onEntered: { - dropDownButton.color = hifi.colors.blueHighlight; - } - onExited: { - dropDownButton.color = hifi.colors.white; - } - onClicked: { - root.category_index = index; - dropdownContainer.visible = false; - } - } - } - Rectangle { - height: 2; - width: parent.width; - color: hifi.colors.lightGray; - visible: model.separator - } - } - } - } - + // or RalewayRegular { id: orText; @@ -283,7 +289,6 @@ Rectangle { font.family: "FiraSans Regular"; font.pixelSize: hifi.fontSizes.textFieldInput; font.capitalization: Font.AllUppercase; - text: root.primaryFilter_displayName; } // workaround for https://bugreports.qt.io/browse/QTBUG-49297 @@ -295,20 +300,22 @@ Rectangle { // emit accepted signal manually if (acceptableInput) { - root.accepted(); - root.forceActiveFocus(); + searchField.accepted(); + searchField.forceActiveFocus(); } break; case Qt.Key_Backspace: - if (textField.text === "") { + if (searchField.text === "") { primaryFilter_index = -1; } break; } } - + onTextChanged: root.searchString = text; onAccepted: { - root.forceActiveFocus(); + root.searchString = searchField.text; + getMarketplaceItems(); + searchField.forceActiveFocus(); } onActiveFocusChanged: { @@ -320,17 +327,579 @@ Rectangle { } } } + // // HEADER BAR END // - DropShadow { - anchors.fill: header; - horizontalOffset: 0; - verticalOffset: 4; - radius: 4.0; - samples: 9 - color: Qt.rgba(0, 0, 0, 0.25); - source: header; - visible: header.visible; + + // + // CATEGORIES LIST START + // + Item { + id: categoriesDropdown; + anchors.fill: parent; + visible: false; + z: 10; + + MouseArea { + anchors.fill: parent; + propagateComposedEvents: true; + onClicked: { + categoriesDropdown.visible = false; + categoriesButton.color = hifi.colors.white; + } + } + + Rectangle { + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.top: parent.top; + anchors.topMargin: 100; + width: parent.width/3; + color: hifi.colors.white; + + ListModel { + id: categoriesModel; + } + + ListView { + id: categoriesListView; + anchors.fill: parent; + anchors.rightMargin: 10; + width: parent.width; + clip: true; + + model: categoriesModel; + delegate: ItemDelegate { + height: 34; + width: parent.width; + clip: true; + contentItem: Rectangle { + id: categoriesItem; + anchors.fill: parent; + color: hifi.colors.white; + visible: true; + + RalewayRegular { + id: categoriesItemText; + text: model.name; + anchors.leftMargin: 15; + anchors.fill:parent; + color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 14; + } + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + z: 10; + onEntered: { + categoriesItem.color = ListView.isCurrentItem ? hifi.colors.white : hifi.colors.lightBlueHighlight; + } + onExited: { + categoriesItem.color = ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.white; + } + onClicked: { + categoriesListView.currentIndex = index; + categoriesText.text = categoriesItemText.text; + categoriesDropdown.visible = false; + categoriesButton.color = hifi.colors.white; + root.categoryString = categoriesItemText.text; + getMarketplaceItems(); + } + } + + } + ScrollBar.vertical: ScrollBar { + parent: categoriesListView.parent; + anchors.top: categoriesListView.top; + anchors.bottom: categoriesListView.bottom; + anchors.left: categoriesListView.right; + contentItem.opacity: 1; + } + } + } + } + // + // CATEGORIES LIST END + // + + // + // ITEMS LIST START + // + Item { + id: itemsList; + anchors.fill: parent; + anchors.topMargin: 100; + anchors.bottomMargin: 50; + visible: true; + + HifiModels.PSFListModel { + id: marketBrowseModel; + itemsPerPage: 7; + listModelName: 'marketBrowse'; + listView: marketBrowseList; + getPage: function () { + + MarketplaceScriptingInterface.getMarketplaceItems( + root.searchString == "" ? undefined : root.searchString, + "", + root.categoryString.toLowerCase(), + "", + "", + root.sortString, + false, + marketBrowseModel.currentPageToRetrieve, + marketBrowseModel.itemsPerPage + ); + } + processPage: function(data) { + console.log(JSON.stringify(data)); + data.items.forEach(function (item) { + console.log(JSON.stringify(item)); + }); + return data.items; + } + } + ListView { + id: marketBrowseList; + model: marketBrowseModel; + // Anchors + anchors.fill: parent; + anchors.rightMargin: 10; + orientation: ListView.Vertical; + focus: true; + clip: true; + + delegate: MarketplaceListItem { + item_id: model.id; + image_url:model.thumbnail_url; + name: model.title; + likes: model.likes; + liked: model.has_liked; + creator: model.creator; + category: model.primary_category; + price: model.cost; + available: model.availability == "available"; + anchors.topMargin: 10; + + + Component.onCompleted: { + console.log("Rendering marketplace list item " + model.id); + console.log(marketBrowseModel.count); + } + + onShowItem: { + MarketplaceScriptingInterface.getMarketplaceItem(item_id); + } + + onBuy: { + sendToScript({method: 'marketplace_checkout', itemId: item_id}); + } + + onCategoryClicked: { + root.categoryString = category; + categoriesText.text = category; + getMarketplaceItems(); + } + + onRequestReload: getMarketplaceItems(); + } + ScrollBar.vertical: ScrollBar { + parent: marketBrowseList.parent; + anchors.top: marketBrowseList.top; + anchors.bottom: marketBrowseList.bottom; + anchors.left: marketBrowseList.right; + contentItem.opacity: 1; + } + headerPositioning: ListView.InlineHeader; + header: Item { + id: itemsHeading; + + height: childrenRect.height; + width: parent.width; + + Item { + id: breadcrumbs; + anchors.left: parent.left; + anchors.right: parent.right; + height: 34; + visible: categoriesListView.currentIndex >= 0; + RalewayRegular { + id: categoriesItemText; + text: "Home /"; + anchors.leftMargin: 15; + anchors.fill:parent; + color: hifi.colors.blueHighlight; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + MouseArea { + anchors.fill: parent; + onClicked: { + categoriesListView.currentIndex = -1; + categoriesText.text = "Categories"; + root.categoryString = ""; + getMarketplaceItems(); + } + } + } + + Item { + id: searchScope; + anchors.top: breadcrumbs.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + RalewaySemiBold { + id: searchScopeText; + text: "Featured"; + anchors.leftMargin: 15; + anchors.fill:parent; + anchors.topMargin: 10; + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 22; + } + } + Item { + id: sort; + anchors.top: searchScope.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.topMargin: 10; + anchors.leftMargin: 15; + height: childrenRect.height; + RalewayRegular { + id: sortText; + text: "Sort:"; + anchors.leftMargin: 15; + anchors.rightMargin: 20; + height: 34; + color: hifi.colors.lightGrayText; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 14; + } + + Rectangle { + radius: 4; + border.width: 1; + border.color: hifi.colors.faintGray; + anchors.left: sortText.right; + anchors.top: parent.top; + width: 322; + height: 36; + anchors.leftMargin: 20; + + + ListModel { + id: sortModel; + ListElement { + name: "Name"; + glyph: ";"; + sortString: "alpha"; + } + ListElement { + name: "Date"; + glyph: ";"; + sortString: "recent"; + } + ListElement { + name: "Popular"; + glyph: ";"; + sortString: "likes"; + } + ListElement { + name: "My Likes"; + glyph: ";"; + sortString: "my_likes"; + } + } + + ListView { + id: sortListView; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.topMargin:1; + anchors.bottomMargin:1; + anchors.leftMargin:1; + anchors.rightMargin:1; + width: childrenRect.width; + height: 34; + orientation: ListView.Horizontal; + focus: true; + clip: true; + highlightFollowsCurrentItem: false; + + delegate: SortButton { + width: 80; + height: parent.height; + glyph: model.glyph; + text: model.name; + + checked: { + ListView.isCurrentItem; + } + onClicked: { + root.currentSortIndex = index; + sortListView.positionViewAtIndex(index, ListView.Beginning); + sortListView.currentIndex = index; + root.sortString = model.sortString; + getMarketplaceItems(); + } + } + highlight: Rectangle { + width: 80; + height: parent.height; + color: hifi.colors.faintGray; + x: sortListView.currentItem.x; + } + model: sortModel; + } + } + } + } + } + } + + // + // ITEMS LIST END + // + + // + // ITEM START + // + Item { + id: marketplaceItemView; + anchors.fill: parent; + width: parent.width; + anchors.topMargin: 120; + visible: false; + + ScrollView { + id: marketplaceItemScrollView; + anchors.fill: parent; + + clip: true; + ScrollBar.vertical.policy: ScrollBar.AlwaysOn; + contentWidth: parent.width; + + Rectangle { + id: marketplaceItemContent; + width: parent.width; + height: childrenRect.height + 100; + + // Title Bar text + RalewaySemiBold { + id: backText; + text: "Back"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left + anchors.leftMargin: 15; + anchors.bottomMargin: 10; + width: paintedWidth; + height: paintedHeight; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + anchors.fill: backText; + + onClicked: { + getMarketplaceItems(); + } + } + + MarketplaceItem { + id: marketplaceItem; + anchors.top: backText.bottom; + width: parent.width; + height: childrenRect.height; + anchors.topMargin: 15; + + onBuy: { + sendToScript({method: 'marketplace_checkout', itemId: item_id}); + } + + onShowLicense: { + licenseInfoWebView.url = url; + licenseInfo.visible = true; + } + onCategoryClicked: { + root.categoryString = category; + categoriesText.text = category; + getMarketplaceItems(); + } + } + } + } + } + // + // ITEM END + // + + // + // FOOTER START + // + + Rectangle { + id: footer; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: 50; + + color: hifi.colors.blueHighlight; + + Item { + anchors.fill: parent; + anchors.rightMargin: 15; + anchors.leftMargin: 15; + + HiFiGlyphs { + id: footerGlyph; + text: hifi.glyphs.info; + // Size + size: 34; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + + anchors.rightMargin: 10; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: footerInfo; + text: "Get items from Clara.io!"; + anchors.left: footerGlyph.right; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + size: 18; + } + + HifiControlsUit.Button { + text: "SEE ALL MARKETS"; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.topMargin: 10; + anchors.bottomMargin: 10; + anchors.leftMargin: 10; + anchors.rightMargin: 10; + width: 180; + + onClicked: { + sendToScript({method: 'marketplace_marketplaces'}); + } + } + } + } + + + // + // FOOTER END + // + + + // + // LICENSE START + // + Rectangle { + id: licenseInfo; + anchors.fill: root; + anchors.topMargin: 100; + anchors.bottomMargin: 0; + visible: false; + + + HifiControlsUit.WebView { + id: licenseInfoWebView; + anchors.bottomMargin: 1; + anchors.topMargin: 50; + anchors.leftMargin: 1; + anchors.rightMargin: 1; + anchors.fill: parent; + } + Item { + id: licenseClose; + anchors.top: parent.top; + anchors.right: parent.right; + anchors.topMargin: 10; + anchors.rightMargin: 10; + + width: 30; + height: 30; + HiFiGlyphs { + anchors.fill: parent; + height: 30; + text: hifi.glyphs.close; + // Size + size: 34; + // Anchors + anchors.rightMargin: 0; + anchors.verticalCenter: parent.verticalCenter; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.baseGray; + } + + MouseArea { + anchors.fill: licenseClose; + onClicked: licenseInfo.visible = false; + } + } + } + // + // LICENSE END + // + + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + + function fromScript(message) { + console.log("FROM SCRIPT " + JSON.stringify(message)); + switch (message.method) { + case 'updateMarketplaceQMLItem': + if (!message.params.itemId) { + console.log("A message with method 'updateMarketplaceQMLItem' was sent without an itemId!"); + return; + } + + MarketplaceScriptingInterface.getMarketplaceItem(message.params.itemId); + break; + } } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml new file mode 100644 index 0000000000..a7f2991920 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -0,0 +1,473 @@ +// +// MarketplaceListItem.qml +// qml/hifi/commerce/marketplace +// +// MarketplaceListItem +// +// Created by Roxanne Skelly on 2019-01-22 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + id: root; + property string item_id: ""; + property string image_url: ""; + property string name: ""; + property int likes: 0; + property bool liked: false; + property string creator: ""; + property var categories: []; + property int price: 0; + property var attributions: []; + property string description: ""; + property string license: ""; + property string posted: ""; + property bool available: false; + property string created_at: ""; + + onCategoriesChanged: { + categoriesListModel.clear(); + categories.forEach(function(category) { + console.log("category is " + category); + categoriesListModel.append({"category":category}); + }); + } + + signal buy(); + signal categoryClicked(string category); + signal showLicense(string url); + + HifiConstants { id: hifi; } + + Connections { + target: MarketplaceScriptingInterface; + + onMarketplaceItemLikeResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Categories", result.data.message); + } else { + root.liked = !root.liked; + root.likes = root.liked ? root.likes + 1 : root.likes - 1; + } + } + } + function getFormattedDate(timestamp) { + function addLeadingZero(n) { + return n < 10 ? '0' + n : '' + n; + } + + var a = new Date(timestamp); + + var year = a.getFullYear(); + var month = addLeadingZero(a.getMonth() + 1); + var day = addLeadingZero(a.getDate()); + var hour = a.getHours(); + var drawnHour = hour; + if (hour === 0) { + drawnHour = 12; + } else if (hour > 12) { + drawnHour -= 12; + } + drawnHour = addLeadingZero(drawnHour); + + var amOrPm = "AM"; + if (hour >= 12) { + amOrPm = "PM"; + } + + var min = addLeadingZero(a.getMinutes()); + var sec = addLeadingZero(a.getSeconds()); + return a.toDateString() + " " + drawnHour + ':' + min + amOrPm; + } + + anchors.left: parent.left; + anchors.right: parent.right; + anchors.leftMargin: 15; + anchors.rightMargin: 15; + height: childrenRect.height; + + + Rectangle { + id: header; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + + height: 50; + + RalewaySemiBold { + id: nameText; + text: root.name; + // Text size + size: 24; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.baseGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + Item { + id: likes; + anchors.top: parent.top; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.rightMargin: 5; + + RalewaySemiBold { + id: heart; + text: "\u2665"; + // Size + size: 20; + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + anchors.rightMargin: 0; + anchors.verticalCenter: parent.verticalCenter; + horizontalAlignment: Text.AlignHCenter; + // Style + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText; + } + + RalewaySemiBold { + id: likesText; + text: root.likes; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.right: heart.left; + anchors.rightMargin: 5; + anchors.leftMargin: 15; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.baseGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + anchors.left: likesText.left; + anchors.right: heart.right; + anchors.top: likesText.top; + anchors.bottom: likesText.bottom; + + onClicked: { + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, !root.liked); + } + } + } + } + Image { + id: itemImage; + source: root.image_url; + anchors.top: header.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: width*0.5625 + fillMode: Image.PreserveAspectCrop; + } + Item { + id: footer; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: itemImage.bottom; + height: childrenRect.height; + + HifiControlsUit.Button { + id: buyButton; + text: root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)"; + enabled: root.available; + buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : ""; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.topMargin: 15; + height: 50; + color: hifi.buttons.blue; + + onClicked: root.buy(); + } + + Item { + id: creatorItem; + anchors.top: buyButton.bottom; + anchors.leftMargin: 15; + anchors.topMargin: 15; + width: parent.width; + height: childrenRect.height; + + RalewaySemiBold { + id: creatorLabel; + text: "CREATOR:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: creatorText; + text: root.creator; + // Text size + size: 18; + // Anchors + anchors.top: creatorLabel.bottom; + anchors.left: parent.left; + anchors.topMargin: 10; + width: paintedWidth; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + } + + Item { + id: posted; + anchors.top: creatorItem.bottom; + anchors.leftMargin: 15; + anchors.topMargin: 15; + width: parent.width; + height: childrenRect.height; + RalewaySemiBold { + id: postedLabel; + text: "POSTED:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: created_at; + text: { getFormattedDate(root.created_at); } + // Text size + size: 14; + // Anchors + anchors.top: postedLabel.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.topMargin: 10; + // Style + color: hifi.colors.lightGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + } + + Rectangle { + anchors.top: posted.bottom; + anchors.leftMargin: 15; + anchors.topMargin: 15; + width: parent.width; + height: 1; + color: hifi.colors.lightGray; + } + + Item { + id: licenseItem; + anchors.top: posted.bottom; + anchors.left: parent.left; + anchors.topMargin: 30; + width: parent.width; + height: childrenRect.height; + RalewaySemiBold { + id: licenseLabel; + text: "SHARED UNDER:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: licenseText; + text: root.license; + // Text size + size: 14; + // Anchors + anchors.top: licenseLabel.bottom; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.lightGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + RalewaySemiBold { + id: licenseHelp; + text: "More about this license"; + // Text size + size: 14; + // Anchors + anchors.top: licenseText.bottom; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + + onClicked: { + licenseInfo.visible = true; + var url; + if (root.license == "No Rights Reserved (CC0)") { + url = "https://creativecommons.org/publicdomain/zero/1.0/" + } else if (root.license == "Attribution (CC BY)") { + url = "https://creativecommons.org/licenses/by/4.0/" + } else if (root.license == "Attribution-ShareAlike (CC BY-SA)") { + url = "https://creativecommons.org/licenses/by-sa/4.0/" + } else if (root.license == "Attribution-NoDerivs (CC BY-ND)") { + url = "https://creativecommons.org/licenses/by-nd/4.0/" + } else if (root.license == "Attribution-NonCommercial (CC BY-NC)") { + url = "https://creativecommons.org/licenses/by-nc/4.0/" + } else if (root.license == "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)") { + url = "https://creativecommons.org/licenses/by-nc-sa/4.0/" + } else if (root.license == "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { + url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" + } else if (root.license == "Proof of Provenance License (PoP License)") { + url = "https://digitalassetregistry.com/PoP-License/v1/" + } + if(url) { + licenseInfoWebView.url = url; + + } + } + } + } + } + + Item { + id: descriptionItem; + anchors.top: licenseItem.bottom; + anchors.topMargin: 15; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + RalewaySemiBold { + id: descriptionLabel; + text: "DESCRIPTION:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + RalewaySemiBold { + id: descriptionText; + text: root.description; + // Text size + size: 14; + // Anchors + anchors.top: descriptionLabel.bottom; + anchors.left: parent.left; + width: parent.width; + // Style + color: hifi.colors.lightGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + wrapMode: Text.Wrap; + } + } + + Item { + id: categoriesList; + anchors.top: descriptionItem.bottom; + anchors.topMargin: 15; + anchors.left: parent.left; + anchors.right: parent.right; + width: parent.width; + height: childrenRect.height; + RalewaySemiBold { + id: categoryLabel; + text: "IN:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + ListModel { + id: categoriesListModel; + } + + ListView { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: categoryLabel.bottom; + model: categoriesListModel; + height: 20*model.count; + delegate: RalewaySemiBold { + id: categoryText; + text: model.category; + // Text size + size: 14; + // Anchors + anchors.leftMargin: 15; + width: paintedWidth; + height: 20; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: categoryText; + onClicked: root.categoryClicked(model.category); + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml new file mode 100644 index 0000000000..4b8471e8b9 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -0,0 +1,278 @@ +// +// MarketplaceListItem.qml +// qml/hifi/commerce/marketplace +// +// MarketplaceListItem +// +// Created by Roxanne Skelly on 2019-01-22 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + id: root; + property string item_id: ""; + property string image_url: ""; + property string name: ""; + property int likes: 0; + property bool liked: false; + property string creator: ""; + property string category: ""; + property int price: 0; + property bool available: false; + + signal buy(); + signal showItem(); + signal categoryClicked(string category); + signal requestReload(); + + HifiConstants { id: hifi; } + + width: parent.width; + height: childrenRect.height+50; + + DropShadow { + anchors.fill: shadowBase; + source: shadowBase; + verticalOffset: 4; + horizontalOffset: 4; + radius: 6; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + + Rectangle { + id: shadowBase; + anchors.fill: itemRect; + color: "white"; + } + + Rectangle { + id: itemRect; + height: childrenRect.height; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.topMargin: 20; + anchors.bottomMargin: 10; + anchors.leftMargin: 15; + anchors.rightMargin: 15; + + MouseArea { + anchors.fill: parent; + onClicked: root.showItem(); + onEntered: { hoverRect.visible = true; console.log("entered"); } + onExited: { hoverRect.visible = false; console.log("exited"); } + hoverEnabled: true + } + + Rectangle { + id: header; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + + height: 50; + + RalewaySemiBold { + id: nameText; + text: root.name; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.rightMargin: 50; + anchors.leftMargin: 15; + anchors.bottom: parent.bottom; + elide: Text.ElideRight; + width: paintedWidth; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + Item { + id: likes; + anchors.top: parent.top; + anchors.right: parent.right; + anchors.rightMargin: 15; + anchors.bottom: parent.bottom; + width: childrenRect.width; + Connections { + target: MarketplaceScriptingInterface; + + onMarketplaceItemLikeResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Categories", result.data.message); + root.requestReload(); + } + } + } + + RalewaySemiBold { + id: heart; + text: "\u2665"; + // Size + size: 20; + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + anchors.rightMargin: 0; + anchors.verticalCenter: parent.verticalCenter; + horizontalAlignment: Text.AlignHCenter; + // Style + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText; + } + + RalewaySemiBold { + id: likesText; + text: root.likes; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.right: heart.left; + anchors.rightMargin: 5; + anchors.leftMargin: 15; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.baseGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + anchors.fill: parent; + onClicked: { + console.log("like " + root.item_id); + root.liked = !root.liked; + root.likes = root.liked ? root.likes + 1 : root.likes - 1; + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); + } + } + } + } + Image { + id: itemImage; + source: root.image_url; + anchors.top: header.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + height: width*0.5625 + fillMode: Image.PreserveAspectCrop; + } + Item { + id: footer; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: itemImage.bottom; + height: 60; + + RalewaySemiBold { + id: creatorLabel; + text: "CREATOR:"; + // Text size + size: 14; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 15; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: creatorText; + text: root.creator; + // Text size + size: 14; + // Anchors + anchors.top: creatorLabel.top; + anchors.left: creatorLabel.right; + anchors.leftMargin: 15; + width: paintedWidth; + // Style + color: hifi.colors.lightGray; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + RalewaySemiBold { + id: categoryLabel; + text: "IN:"; + // Text size + size: 14; + // Anchors + anchors.top: creatorLabel.bottom; + anchors.left: parent.left; + anchors.leftMargin: 15; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: categoryText; + text: root.category; + // Text size + size: 14; + // Anchors + anchors.top: categoryLabel.top; + anchors.left: categoryLabel.right; + anchors.leftMargin: 15; + width: paintedWidth; + // Style + color: hifi.colors.blueHighlight; + // Alignment + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + onClicked: root.categoryClicked(root.category); + } + } + + HifiControlsUit.Button { + text: root.price ? root.price : "FREE"; + buttonGlyph: root.price ? hifi.glyphs.hfc : ""; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.rightMargin: 15; + anchors.topMargin:10; + anchors.bottomMargin: 10; + color: hifi.buttons.blue; + + onClicked: root.buy(); + } + } + Rectangle { + id: hoverRect; + anchors.fill: parent; + border.color: hifi.colors.blueHighlight; + border.width: 2; + color: "#00000000"; + visible: false; + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml new file mode 100644 index 0000000000..37ad2735ce --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml @@ -0,0 +1,87 @@ +// +// SortButton.qml +// qml/hifi/commerce/marketplace +// +// SortButton +// +// Created by Roxanne Skelly on 2019-01-18 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Item { + HifiConstants { id: hifi; } + + id: root; + + + property string glyph: ""; + property string text: ""; + property bool checked: false; + signal clicked(); + + width: childrenRect.width; + height: parent.height; + + Rectangle { + anchors.top: parent.top; + anchors.left: parent.left; + height: parent.height; + width: 2; + color: hifi.colors.faintGray; + visible: index > 0; + } + + HiFiGlyphs { + id: buttonGlyph; + text: root.glyph; + // Size + size: 14; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 0; + anchors.top: parent.top; + anchors.verticalCenter: parent.verticalCenter; + height: parent.height; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.lightGray; + } + RalewayRegular { + id: buttonText; + text: root.text; + // Text size + size: 14; + // Style + color: hifi.colors.lightGray; + elide: Text.ElideRight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + height: parent.height; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.clicked(); + } + } +} \ No newline at end of file diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1ec9c93e12..88fda37623 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2916,7 +2916,7 @@ void Application::initializeUi() { }, commerceCallback); QmlContextCallback marketplaceCallback = [](QQmlContext* context) { - context->setContextProperty("Marketplace", new QmlMarketplace()); + context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); }; OffscreenQmlSurface::addWhitelistContextHandler({ QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp index 99d3bb1ae6..07a9e570bd 100644 --- a/interface/src/commerce/QmlMarketplace.cpp +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -55,50 +55,54 @@ void QmlMarketplace::getMarketplaceItems( const int& perPage) { QString endpoint = "items"; - QJsonObject request; - request["q"] = q; - request["view"] = view; - request["category"] = category; - request["adminFilter"] = adminFilter; - request["adminFilterCost"] = adminFilterCost; - request["sort"] = sort; - request["isFree"] = isFree; - request["page"] = page; - request["perPage"] = perPage; + QUrlQuery request; + request.addQueryItem("q", q); + request.addQueryItem("view", view); + request.addQueryItem("category", category); + request.addQueryItem("adminFilter", adminFilter); + request.addQueryItem("adminFilterCost", adminFilterCost); + request.addQueryItem("sort", sort); + if (isFree) { + request.addQueryItem("isFree", "true"); + } + request.addQueryItem("page", QString::number(page)); + request.addQueryItem("perPage", QString::number(perPage)); send(endpoint, "getMarketplaceItemsSuccess", "getMarketplaceItemsFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); } void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) { QString endpoint = QString("items/") + marketplaceItemId; - QJsonObject request; + QUrlQuery request; send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); } void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) { QString endpoint = QString("items/") + marketplaceItemId + "/like"; - QJsonObject request; - send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PutOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); + QUrlQuery request; + send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); } void QmlMarketplace::getMarketplaceCategories() { QString endpoint = "categories"; - QJsonObject request; + QUrlQuery request; send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request); } -void QmlMarketplace::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) { +void QmlMarketplace::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request) { auto accountManager = DependencyManager::get(); const QString URL = "/api/v1/marketplace/"; JSONCallbackParameters callbackParams(this, success, fail); -#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. - qCInfo(commerce) << "Sending" << QJsonDocument(request).toJson(QJsonDocument::Compact); -#endif + accountManager->sendRequest(URL + endpoint, authType, method, callbackParams, - QJsonDocument(request).toJson()); + QByteArray(), + NULL, + QVariantMap(), + request); + } QJsonObject QmlMarketplace::apiResponse(const QString& label, QNetworkReply* reply) { diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h index 95a1aa3911..f954198371 100644 --- a/interface/src/commerce/QmlMarketplace.h +++ b/interface/src/commerce/QmlMarketplace.h @@ -60,7 +60,7 @@ signals: void marketplaceItemLikeResult(QJsonObject result); private: - void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request); + void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request); QJsonObject apiResponse(const QString& label, QNetworkReply* reply); QJsonObject failResponse(const QString& label, QNetworkReply* reply); }; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 989661cb81..5be2c6d02e 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -208,7 +208,7 @@ void AccountManager::setSessionID(const QUuid& sessionID) { } } -QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) { +QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query) { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); @@ -227,6 +227,7 @@ QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth:: } else { requestURL.setPath("/" + path); } + requestURL.setQuery(query); if (authType != AccountManagerAuth::None ) { if (hasValidAccessToken()) { @@ -263,13 +264,14 @@ void AccountManager::sendRequest(const QString& path, Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart), - Q_ARG(QVariantMap, propertyMap)); + Q_ARG(QVariantMap, propertyMap), + Q_ARG(QUrlQuery, query)); return; } QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = createRequest(path, authType); + QNetworkRequest networkRequest = createRequest(path, authType, query); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString()); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index ca2b826c98..36563a6ae0 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -61,7 +61,7 @@ class AccountManager : public QObject, public Dependency { public: AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); - QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType); + QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query = QUrlQuery()); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 19efdc042c..7cacfd7935 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -379,21 +379,16 @@ function deleteSendMoneyParticleEffect() { function onUsernameChanged() { } -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var METAVERSE_SERVER_URL = Account.metaverseServerURL; -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. -function openMarketplace(optionalItemOrUrl) { - // This is a bit of a kluge, but so is the whole file. - // If given a whole path, use it with no cta. - // If given an id, build the appropriate url and use the id as the cta. - // Otherwise, use home and 'marketplace cta'. - // AND... if call onMarketplaceOpen to setupWallet if we need to. - var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; - // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. - if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { - url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; +var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; +function openMarketplace(optionalItem) { + ui.open(MARKETPLACE_QML_PATH); + + if (optionalItem) { + ui.tablet.sendToQml({ + method: 'updateMarketplaceQMLItem', + params: { itemId: optionalItem } + }); } - ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } function setCertificateInfo(itemCertificateId) { diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index f1931192e4..74bf8d3fec 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -19,6 +19,7 @@ var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; @@ -72,7 +73,13 @@ // Footer actions. $("#back-button").on("click", function () { - (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); + if (document.referrer !== "") { + window.history.back(); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MARKETPLACE + })); + } }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(JSON.stringify({ @@ -93,7 +100,9 @@ window.location = "https://clara.io/library?gameCheck=true&public=true"; }); $('#exploreHifiMarketplace').on('click', function () { - window.location = marketplaceBaseURL + "/marketplace?"; + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MARKETPLACE + })); }); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index db3b2e2107..2ca79b49f0 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -25,6 +25,7 @@ var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertif var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; // HRS FIXME "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; +var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var METAVERSE_SERVER_URL = Account.metaverseServerURL; @@ -36,6 +37,7 @@ var CLARA_IO_STATUS = "CLARA.IO STATUS"; var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; var GOTO_DIRECTORY = "GOTO_DIRECTORY"; +var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; @@ -154,18 +156,15 @@ function onMarketplaceOpen(referrer) { } } -function openMarketplace(optionalItemOrUrl) { - // This is a bit of a kluge, but so is the whole file. - // If given a whole path, use it with no cta. - // If given an id, build the appropriate url and use the id as the cta. - // Otherwise, use home and 'marketplace cta'. - // AND... if call onMarketplaceOpen to setupWallet if we need to. - var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; - // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. - if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { - url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; +function openMarketplace(optionalItem) { + ui.open(MARKETPLACE_QML_PATH); + + if (optionalItem) { + ui.tablet.sendToQml({ + method: 'updateMarketplaceQMLItem', + params: { itemId: optionalItem } + }); } - ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } // Function Name: wireQmlEventBridge() @@ -439,7 +438,9 @@ var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onWebEventReceived(message) { message = JSON.parse(message); - if (message.type === GOTO_DIRECTORY) { + if (message.type === GOTO_MARKETPLACE) { + openMarketplace(); + } else if (message.type === GOTO_DIRECTORY) { // This is the chooser between marketplaces. Only OUR markteplace // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -518,6 +519,7 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { if (message.messageSrc === "HTML") { return; } + console.log(JSON.stringify(message)); switch (message.method) { case 'gotoBank': ui.close(); @@ -560,6 +562,18 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_continue': openMarketplace(); break; + case 'marketplace_checkout': + ui.open(MARKETPLACE_CHECKOUT_QML_PATH); + ui.tablet.sendToQml({ + method: 'updateCheckoutQMLItemID', + params: message + }); + break; + case 'marketplace_marketplaces': + // This is the chooser between marketplaces. Only OUR markteplace + // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. + ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': @@ -699,7 +713,9 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var onMarketplaceItemTesterScreenNow = ( url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || url === MARKETPLACE_ITEM_TESTER_QML_PATH); - + var onMarketplaceScreenNow = ( + url.indexOf(MARKETPLACE_QML_PATH) !== -1 || + url === MARKETPLACE_QML_PATH); if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen) || (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) @@ -711,7 +727,7 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { onCommerceScreen = onCommerceScreenNow; onWalletScreen = onWalletScreenNow; onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; - wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen); + wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen || onMarketplaceScreenNow); if (url === MARKETPLACE_PURCHASES_QML_PATH) { // FIXME? Is there a race condition here in which the event @@ -769,14 +785,13 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var BUTTON_NAME = "MARKET"; -var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml"; var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 9, - home: MARKETPLACE_QML_SOURCE, + home: MARKETPLACE_QML_PATH, onScreenChanged: onTabletScreenChanged, onMessage: onQmlMessageReceived }); From 50e7adf32c52cf1a253c6c5d92dd8d07b34d3c51 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 24 Jan 2019 17:55:00 -0800 Subject: [PATCH 005/130] Can download Build XML in Test on Mobile mode. --- tools/nitpick/src/Nitpick.cpp | 24 ++++++++++++++++++++++-- tools/nitpick/src/TestRunner.cpp | 18 ++++++++++++++++++ tools/nitpick/src/TestRunner.h | 6 ++++++ tools/nitpick/src/TestRunnerDesktop.cpp | 19 ++++--------------- tools/nitpick/src/TestRunnerDesktop.h | 4 ---- tools/nitpick/src/TestRunnerMobile.cpp | 18 ++++++++++++++++-- tools/nitpick/src/TestRunnerMobile.h | 5 ++++- 7 files changed, 70 insertions(+), 24 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 12c038a50f..ccbb2da762 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -88,12 +88,28 @@ void Nitpick::setup() { if (_testRunnerDesktop) { delete _testRunnerDesktop; } - _testRunnerDesktop = new TestRunnerDesktop(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderRunOnDesktopLabel, _ui.checkBoxServerless, _ui.runLatestOnDesktopCheckBox, _ui.urlOnDesktopLineEdit, _ui.runNowPushbutton); + _testRunnerDesktop = new TestRunnerDesktop( + dayCheckboxes, + timeEditCheckboxes, + timeEdits, + _ui.workingFolderRunOnDesktopLabel, + _ui.checkBoxServerless, + _ui.runLatestOnDesktopCheckBox, + _ui.urlOnDesktopLineEdit, + _ui.runNowPushbutton + ); if (_testRunnerMobile) { delete _testRunnerMobile; } - _testRunnerMobile = new TestRunnerMobile(_ui.workingFolderRunOnMobileLabel, _ui.connectDevicePushbutton, _ui.pullFolderPushbutton, _ui.detectedDeviceLabel, _ui.folderLineEdit); + _testRunnerMobile = new TestRunnerMobile( + _ui.workingFolderRunOnMobileLabel, + _ui.connectDevicePushbutton, + _ui.pullFolderPushbutton, + _ui.detectedDeviceLabel, + _ui.folderLineEdit, + _ui.downloadAPKPushbutton + ); } void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, @@ -285,7 +301,11 @@ void Nitpick::saveFile(int index) { _test->finishTestsEvaluation(); } else if (_caller == _testRunnerDesktop) { _testRunnerDesktop->downloadComplete(); + } else if (_caller == _testRunnerMobile) { + _testRunnerMobile->downloadComplete(); } + + _ui.progressBar->setVisible(false); } else { _ui.progressBar->setValue(_numberOfFilesDownloaded); } diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 491e211e2c..3089b59e2c 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -11,6 +11,9 @@ #include +#include "Nitpick.h" +extern Nitpick* nitpick; + void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { // Everything will be written to this folder QString previousSelection = _workingFolder; @@ -31,6 +34,21 @@ void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); } +void TestRunner::downloadBuildXml(void* caller) { + // Download the latest High Fidelity build XML. + // Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked) + // It is still downloaded, to simplify the flow + buildXMLDownloaded = false; + + QStringList urls; + QStringList filenames; + + urls << DEV_BUILD_XML_URL; + filenames << DEV_BUILD_XML_FILENAME; + + nitpick->downloadFiles(urls, _workingFolder, filenames, caller); +} + void Worker::setCommandLine(const QString& commandLine) { _commandLine = commandLine; } diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index 338cb5524e..40f8a7d95a 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -19,9 +19,15 @@ class Worker; class TestRunner { public: void setWorkingFolder(QLabel* workingFolderLabel); + void downloadBuildXml(void* caller); protected: QString _workingFolder; + + const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; + const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; + + bool buildXMLDownloaded; }; class Worker : public QObject { diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp index f0147f200d..6ddf452b0a 100644 --- a/tools/nitpick/src/TestRunnerDesktop.cpp +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -13,14 +13,14 @@ #include #include -#include "Nitpick.h" -extern Nitpick* nitpick; - #ifdef Q_OS_WIN #include #include #endif +#include "Nitpick.h" +extern Nitpick* nitpick; + TestRunnerDesktop::TestRunnerDesktop(std::vector dayCheckboxes, std::vector timeEditCheckboxes, std::vector timeEdits, @@ -176,19 +176,8 @@ void TestRunnerDesktop::run() { // This will be restored at the end of the tests saveExistingHighFidelityAppDataFolder(); - // Download the latest High Fidelity build XML. - // Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked) - // It is still downloaded, to simplify the flow - QStringList urls; - QStringList filenames; - - urls << DEV_BUILD_XML_URL; - filenames << DEV_BUILD_XML_FILENAME; - updateStatusLabel("Downloading Build XML"); - - buildXMLDownloaded = false; - nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + downloadBuildXml((void*)this); // `downloadComplete` will run after download has completed } diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h index 1cc93c3ad5..467162da9f 100644 --- a/tools/nitpick/src/TestRunnerDesktop.h +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -102,10 +102,6 @@ private: QString _installerURL; QString _installerFilename; - const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; - const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; - - bool buildXMLDownloaded; QDir _appDataFolder; QDir _savedAppDataFolder; diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index f27e161a87..a269c15b26 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -22,6 +22,7 @@ TestRunnerMobile::TestRunnerMobile( QPushButton *pullFolderButton, QLabel* detectedDeviceLabel, QLineEdit *folderLineEdit, + QPushButton* downloadAPKPushbutton, QObject* parent ) : QObject(parent) { @@ -30,6 +31,7 @@ TestRunnerMobile::TestRunnerMobile( _pullFolderButton = pullFolderButton; _detectedDeviceLabel = detectedDeviceLabel; _folderLineEdit = folderLineEdit; + _downloadAPKPushbutton = downloadAPKPushbutton; folderLineEdit->setText("/sdcard/DCIM/TEST"); } @@ -41,6 +43,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { setWorkingFolder(_workingFolderLabel); _connectDeviceButton->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); // Find ADB (Android Debugging Bridge) before continuing #ifdef Q_OS_WIN @@ -70,7 +73,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { void TestRunnerMobile::connectDevice() { QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbCommand + " devices > " + devicesFullFilename; - int result = system(command.toStdString().c_str()); + system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); @@ -99,9 +102,20 @@ void TestRunnerMobile::connectDevice() { } void TestRunnerMobile::downloadAPK() { + downloadBuildXml((void*)this); } + +void TestRunnerMobile::downloadComplete() { + if (!buildXMLDownloaded) { + // Download of Build XML has completed + buildXMLDownloaded = true; + + // Download the High Fidelity APK + int df = 546; + } +} void TestRunnerMobile::pullFolder() { QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + " >" + _workingFolder + "/pullOutput.txt"; - int result = system(command.toStdString().c_str()); + system(command.toStdString().c_str()); } diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 1f6b72cfd8..f0d79ae177 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -26,7 +26,8 @@ public: QPushButton *connectDeviceButton, QPushButton *pullFolderButton, QLabel* detectedDeviceLabel, - QLineEdit *folderLineEdit, + QLineEdit* folderLineEdit, + QPushButton* downloadAPKPushbutton, QObject* parent = 0 ); ~TestRunnerMobile(); @@ -34,6 +35,7 @@ public: void setWorkingFolderAndEnableControls(); void connectDevice(); void downloadAPK(); + void downloadComplete(); void pullFolder(); private: @@ -42,6 +44,7 @@ private: QPushButton* _pullFolderButton; QLabel* _detectedDeviceLabel; QLineEdit* _folderLineEdit; + QPushButton* _downloadAPKPushbutton; #ifdef Q_OS_WIN const QString _adbExe{ "adb.exe" }; From 1c1b34c3aa9cbdf37287a4be2309d4889a518e45 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 11:30:43 -0800 Subject: [PATCH 006/130] WIP - downloading Installer (APK) --- tools/nitpick/src/Nitpick.cpp | 17 +-- tools/nitpick/src/Nitpick.h | 1 - tools/nitpick/src/TestRunner.cpp | 103 +++++++++++++++++- tools/nitpick/src/TestRunner.h | 24 ++++ tools/nitpick/src/TestRunnerDesktop.cpp | 139 ++++-------------------- tools/nitpick/src/TestRunnerDesktop.h | 46 +++----- tools/nitpick/src/TestRunnerMobile.cpp | 43 +++++++- tools/nitpick/src/TestRunnerMobile.h | 17 +-- tools/nitpick/ui/Nitpick.ui | 28 ++++- 9 files changed, 247 insertions(+), 171 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index ccbb2da762..6db107b9a7 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -35,8 +35,10 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.tabWidget->removeTab(1); #endif - _ui.statusLabel->setText(""); - _ui.plainTextEdit->setReadOnly(true); + _ui.statusLabelOnDesktop->setText(""); + _ui.statusLabelOnMobile->setText(""); + + _ui.plainTextEdit->setReadOnly(true); setWindowTitle("Nitpick - v2.0.1"); } @@ -96,7 +98,8 @@ void Nitpick::setup() { _ui.checkBoxServerless, _ui.runLatestOnDesktopCheckBox, _ui.urlOnDesktopLineEdit, - _ui.runNowPushbutton + _ui.runNowPushbutton, + _ui.statusLabelOnDesktop ); if (_testRunnerMobile) { @@ -108,7 +111,9 @@ void Nitpick::setup() { _ui.pullFolderPushbutton, _ui.detectedDeviceLabel, _ui.folderLineEdit, - _ui.downloadAPKPushbutton + _ui.downloadAPKPushbutton, + _ui.runLatestOnMobileCheckBox, + _ui.urlOnMobileLineEdit ); } @@ -335,10 +340,6 @@ QString Nitpick::getSelectedBranch() { return _ui.branchLineEdit->text(); } -void Nitpick::updateStatusLabel(const QString& status) { - _ui.statusLabel->setText(status); -} - void Nitpick::appendLogWindow(const QString& message) { _ui.plainTextEdit->appendPlainText(message); } diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 18916a1f03..f54754de2e 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -51,7 +51,6 @@ public: void enableRunTabControls(); - void updateStatusLabel(const QString& status); void appendLogWindow(const QString& message); private slots: diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 3089b59e2c..6d3a3e1b84 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -49,6 +49,108 @@ void TestRunner::downloadBuildXml(void* caller) { nitpick->downloadFiles(urls, _workingFolder, filenames, caller); } +void TestRunner::parseBuildInformation() { + try { + QDomDocument domDocument; + QString filename{ _workingFolder + "/" + DEV_BUILD_XML_FILENAME }; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly) || !domDocument.setContent(&file)) { + throw QString("Could not open " + filename); + } + + QString platformOfInterest; +#ifdef Q_OS_WIN + platformOfInterest = "windows"; +#elif defined(Q_OS_MAC) + platformOfInterest = "mac"; +#endif + + QDomElement element = domDocument.documentElement(); + + // Verify first element is "projects" + if (element.tagName() != "projects") { + throw("File seems to be in wrong format"); + } + + element = element.firstChild().toElement(); + if (element.tagName() != "project") { + throw("File seems to be in wrong format"); + } + + if (element.attribute("name") != "interface") { + throw("File is not from 'interface' build"); + } + + // Now loop over the platforms, looking for ours + bool platformFound{ false }; + element = element.firstChild().toElement(); + while (!element.isNull()) { + if (element.attribute("name") == platformOfInterest) { + platformFound = true; + break; + } + element = element.nextSibling().toElement(); + } + + if (!platformFound) { + throw("File seems to be in wrong format - platform " + platformOfInterest + " not found"); + } + + element = element.firstChild().toElement(); + if (element.tagName() != "build") { + throw("File seems to be in wrong format"); + } + + // Next element should be the version + element = element.firstChild().toElement(); + if (element.tagName() != "version") { + throw("File seems to be in wrong format"); + } + + // Add the build number to the end of the filename + _buildInformation.build = element.text(); + + // First sibling should be stable_version + element = element.nextSibling().toElement(); + if (element.tagName() != "stable_version") { + throw("File seems to be in wrong format"); + } + + // Next sibling should be url + element = element.nextSibling().toElement(); + if (element.tagName() != "url") { + throw("File seems to be in wrong format"); + } + _buildInformation.url = element.text(); + + } + catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } + catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +QString TestRunner::getInstallerNameFromURL(const QString& url) { + // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe + // On Mac, replace `exe` with `dmg` + try { + QStringList urlParts = url.split("/"); + return urlParts[urlParts.size() - 1]; + } + catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } + catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + void Worker::setCommandLine(const QString& commandLine) { _commandLine = commandLine; } @@ -58,4 +160,3 @@ int Worker::runCommand() { emit commandComplete(); return result; } - diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index 40f8a7d95a..0453bcf491 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -11,23 +11,47 @@ #ifndef hifi_testRunner_h #define hifi_testRunner_h +#include #include +#include #include class Worker; +class BuildInformation { +public: + QString build; + QString url; +}; + class TestRunner { public: void setWorkingFolder(QLabel* workingFolderLabel); void downloadBuildXml(void* caller); + void parseBuildInformation(); + QString getInstallerNameFromURL(const QString& url); protected: + QLabel* _workingFolderLabel; + QLabel* _statusLabel; + QLineEdit* _url; + QCheckBox* _runLatest; + QString _workingFolder; const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; bool buildXMLDownloaded; + BuildInformation _buildInformation; + +#ifdef Q_OS_WIN + const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; +#elif defined(Q_OS_MAC) + const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.dmg" }; +#else + const QString INSTALLER_FILENAME_LATEST{ "" }; +#endif }; class Worker : public QObject { diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp index 6ddf452b0a..eb371f7e85 100644 --- a/tools/nitpick/src/TestRunnerDesktop.cpp +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -21,16 +21,20 @@ #include "Nitpick.h" extern Nitpick* nitpick; -TestRunnerDesktop::TestRunnerDesktop(std::vector dayCheckboxes, - std::vector timeEditCheckboxes, - std::vector timeEdits, - QLabel* workingFolderLabel, - QCheckBox* runServerless, - QCheckBox* runLatest, - QLineEdit* url, - QPushButton* runNow, - QObject* parent) : - QObject(parent) { +TestRunnerDesktop::TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent +) : QObject(parent) +{ _dayCheckboxes = dayCheckboxes; _timeEditCheckboxes = timeEditCheckboxes; _timeEdits = timeEdits; @@ -39,6 +43,7 @@ TestRunnerDesktop::TestRunnerDesktop(std::vector dayCheckboxes, _runLatest = runLatest; _url = url; _runNow = runNow; + _statusLabel = statusLabel; _installerThread = new QThread(); _installerWorker = new InstallerWorker(); @@ -176,7 +181,7 @@ void TestRunnerDesktop::run() { // This will be restored at the end of the tests saveExistingHighFidelityAppDataFolder(); - updateStatusLabel("Downloading Build XML"); + _statusLabel->setText("Downloading Build XML"); downloadBuildXml((void*)this); // `downloadComplete` will run after download has completed @@ -204,7 +209,7 @@ void TestRunnerDesktop::downloadComplete() { filenames << _installerFilename; } - updateStatusLabel("Downloading installer"); + _statusLabel->setText("Downloading installer"); nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); @@ -216,7 +221,7 @@ void TestRunnerDesktop::downloadComplete() { QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + _testStartDateTime.date().toString("ddd, MMM d, yyyy")); - updateStatusLabel("Installing"); + _statusLabel->setText("Installing"); // Kill any existing processes that would interfere with installation killProcesses(); @@ -278,7 +283,7 @@ void TestRunnerDesktop::installationComplete() { createSnapshotFolder(); - updateStatusLabel("Running tests"); + _statusLabel->setText("Running tests"); if (!_runServerless->isChecked()) { startLocalServerProcesses(); @@ -547,7 +552,7 @@ void TestRunnerDesktop::interfaceExecutionComplete() { } void TestRunnerDesktop::evaluateResults() { - updateStatusLabel("Evaluating results"); + _statusLabel->setText("Evaluating results"); nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); } @@ -555,7 +560,7 @@ void TestRunnerDesktop::automaticTestRunEvaluationComplete(QString zippedFolder, addBuildNumberToResults(zippedFolder); restoreHighFidelityAppDataFolder(); - updateStatusLabel("Testing complete"); + _statusLabel->setText("Testing complete"); QDateTime currentDateTime = QDateTime::currentDateTime(); @@ -656,10 +661,6 @@ void TestRunnerDesktop::checkTime() { } } -void TestRunnerDesktop::updateStatusLabel(const QString& message) { - nitpick->updateStatusLabel(message); -} - void TestRunnerDesktop::appendLog(const QString& message) { if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), @@ -674,21 +675,6 @@ void TestRunnerDesktop::appendLog(const QString& message) { nitpick->appendLogWindow(message); } -QString TestRunnerDesktop::getInstallerNameFromURL(const QString& url) { - // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe - // On Mac, replace `exe` with `dmg` - try { - QStringList urlParts = url.split("/"); - return urlParts[urlParts.size() - 1]; - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } -} - QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { try { QStringList urlParts = url.split("/"); @@ -709,86 +695,3 @@ QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { exit(-1); } } - -void TestRunnerDesktop::parseBuildInformation() { - try { - QDomDocument domDocument; - QString filename{ _workingFolder + "/" + DEV_BUILD_XML_FILENAME }; - QFile file(filename); - if (!file.open(QIODevice::ReadOnly) || !domDocument.setContent(&file)) { - throw QString("Could not open " + filename); - } - - QString platformOfInterest; -#ifdef Q_OS_WIN - platformOfInterest = "windows"; -#elif defined(Q_OS_MAC) - platformOfInterest = "mac"; -#endif - - QDomElement element = domDocument.documentElement(); - - // Verify first element is "projects" - if (element.tagName() != "projects") { - throw("File seems to be in wrong format"); - } - - element = element.firstChild().toElement(); - if (element.tagName() != "project") { - throw("File seems to be in wrong format"); - } - - if (element.attribute("name") != "interface") { - throw("File is not from 'interface' build"); - } - - // Now loop over the platforms, looking for ours - bool platformFound{ false }; - element = element.firstChild().toElement(); - while (!element.isNull()) { - if (element.attribute("name") == platformOfInterest) { - platformFound = true; - break; - } - element = element.nextSibling().toElement(); - } - - if (!platformFound) { - throw("File seems to be in wrong format - platform " + platformOfInterest + " not found"); - } - - element = element.firstChild().toElement(); - if (element.tagName() != "build") { - throw("File seems to be in wrong format"); - } - - // Next element should be the version - element = element.firstChild().toElement(); - if (element.tagName() != "version") { - throw("File seems to be in wrong format"); - } - - // Add the build number to the end of the filename - _buildInformation.build = element.text(); - - // First sibling should be stable_version - element = element.nextSibling().toElement(); - if (element.tagName() != "stable_version") { - throw("File seems to be in wrong format"); - } - - // Next sibling should be url - element = element.nextSibling().toElement(); - if (element.tagName() != "url") { - throw("File seems to be in wrong format"); - } - _buildInformation.url = element.text(); - - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } -} diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h index 467162da9f..83551cef0e 100644 --- a/tools/nitpick/src/TestRunnerDesktop.h +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -11,10 +11,8 @@ #ifndef hifi_testRunnerDesktop_h #define hifi_testRunnerDesktop_h -#include #include #include -#include #include #include #include @@ -23,27 +21,25 @@ #include "TestRunner.h" -class BuildInformation { -public: - QString build; - QString url; -}; - class InterfaceWorker; class InstallerWorker; class TestRunnerDesktop : public QObject, public TestRunner { Q_OBJECT public: - explicit TestRunnerDesktop(std::vector dayCheckboxes, - std::vector timeEditCheckboxes, - std::vector timeEdits, - QLabel* workingFolderLabel, - QCheckBox* runServerless, - QCheckBox* runLatest, - QLineEdit* url, - QPushButton* runNow, - QObject* parent = 0); + explicit TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent = 0 + ); ~TestRunnerDesktop(); @@ -71,14 +67,10 @@ public: void copyFolder(const QString& source, const QString& destination); - void updateStatusLabel(const QString& message); void appendLog(const QString& message); - QString getInstallerNameFromURL(const QString& url); QString getPRNumberFromURL(const QString& url); - void parseBuildInformation(); - private slots: void checkTime(); void installationComplete(); @@ -92,14 +84,6 @@ signals: private: bool _automatedTestIsRunning{ false }; -#ifdef Q_OS_WIN - const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; -#elif defined(Q_OS_MAC) - const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.dmg" }; -#else - const QString INSTALLER_FILENAME_LATEST{ "" }; -#endif - QString _installerURL; QString _installerFilename; @@ -120,8 +104,6 @@ private: std::vector _timeEdits; QLabel* _workingFolderLabel; QCheckBox* _runServerless; - QCheckBox* _runLatest; - QLineEdit* _url; QPushButton* _runNow; QTimer* _timer; @@ -134,8 +116,6 @@ private: InstallerWorker* _installerWorker; InterfaceWorker* _interfaceWorker; - - BuildInformation _buildInformation; }; class InstallerWorker : public Worker { diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index a269c15b26..85aaeec609 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -23,6 +23,9 @@ TestRunnerMobile::TestRunnerMobile( QLabel* detectedDeviceLabel, QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QObject* parent ) : QObject(parent) { @@ -32,6 +35,8 @@ TestRunnerMobile::TestRunnerMobile( _detectedDeviceLabel = detectedDeviceLabel; _folderLineEdit = folderLineEdit; _downloadAPKPushbutton = downloadAPKPushbutton; + _runLatest = runLatest; + _url = url; folderLineEdit->setText("/sdcard/DCIM/TEST"); } @@ -111,10 +116,44 @@ void TestRunnerMobile::downloadComplete() { // Download of Build XML has completed buildXMLDownloaded = true; - // Download the High Fidelity APK - int df = 546; + // Download the High Fidelity installer + QStringList urls; + QStringList filenames; + if (_runLatest->isChecked()) { + parseBuildInformation(); + + _installerFilename = INSTALLER_FILENAME_LATEST; + + urls << _buildInformation.url; + filenames << _installerFilename; + } else { + QString urlText = _url->text(); + urls << urlText; + _installerFilename = getInstallerNameFromURL(urlText); + filenames << _installerFilename; + } + + _statusLabel->setText("Downloading installer"); + + //// nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + + // `downloadComplete` will run again after download has completed + + } else { + // Download of Installer has completed +//// appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" + +//// QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + +//// _testStartDateTime.date().toString("ddd, MMM d, yyyy")); + + _statusLabel->setText("Installing"); + + // Kill any existing processes that would interfere with installation +//// killProcesses(); + +//// runInstaller(); } } + void TestRunnerMobile::pullFolder() { QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + " >" + _workingFolder + "/pullOutput.txt"; system(command.toStdString().c_str()); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index f0d79ae177..8d08eda191 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -12,7 +12,6 @@ #define hifi_testRunnerMobile_h #include -#include #include #include @@ -22,12 +21,15 @@ class TestRunnerMobile : public QObject, public TestRunner { Q_OBJECT public: explicit TestRunnerMobile( - QLabel* workingFolderLabel, - QPushButton *connectDeviceButton, - QPushButton *pullFolderButton, - QLabel* detectedDeviceLabel, - QLineEdit* folderLineEdit, + QLabel* workingFolderLabel, + QPushButton *connectDeviceButton, + QPushButton *pullFolderButton, + QLabel* detectedDeviceLabel, + QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QObject* parent = 0 ); ~TestRunnerMobile(); @@ -39,7 +41,6 @@ public: void pullFolder(); private: - QLabel* _workingFolderLabel; QPushButton* _connectDeviceButton; QPushButton* _pullFolderButton; QLabel* _detectedDeviceLabel; @@ -53,6 +54,8 @@ private: const QString _adbExe{ "adb" }; #endif + QString _installerFilename; + QString _adbCommand; }; diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 78d9e8613e..1fa2b3f9d7 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -469,7 +469,7 @@ Status: - + 350 @@ -683,6 +683,32 @@ Download APK + + + + 290 + 20 + 41 + 31 + + + + Status: + + + + + + 340 + 20 + 271 + 31 + + + + ####### + + From b95cc149aacb5e182a9c171884067441c3850384 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 13:00:17 -0800 Subject: [PATCH 007/130] Corrected miss-named Pushbutton. --- tools/nitpick/ui/Nitpick.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 1fa2b3f9d7..50d6a1f661 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -841,7 +841,7 @@ Amazon Web Services - + true @@ -1052,7 +1052,7 @@ createTestRailRunPushbutton createTestRailTestCasesPushbutton createXMLScriptRadioButton - createWebPagePushPushbutton + createWebPagePushbutton updateAWSCheckBox awsURLLineEdit closePushbutton From ff2d51701e6589a7ac2c99b668cdeaf429ab5935 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 25 Jan 2019 13:41:16 -0800 Subject: [PATCH 008/130] QML Marketplace Add keyboard while in search General cleanup --- .../hifi/commerce/marketplace/Marketplace.qml | 996 ++++++++++-------- .../commerce/marketplace/MarketplaceItem.qml | 577 +++++----- .../marketplace/MarketplaceListItem.qml | 376 ++++--- 3 files changed, 1058 insertions(+), 891 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index e5ce431de8..77820ccaca 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -24,18 +24,21 @@ import "../common/sendAsset" import "../.." as HifiCommon Rectangle { - HifiConstants { id: hifi; } + HifiConstants { + id: hifi + } - id: root; + id: root - property string activeView: "initialize"; - property bool keyboardRaised: false; - property int currentSortIndex: 0; - property string sortString: ""; - property string categoryString: ""; - property string searchString: ""; - - anchors.fill: (typeof parent === undefined) ? undefined : parent; + property string activeView: "initialize" + property int currentSortIndex: 0 + property string sortString: "" + property string categoryString: "" + property string searchString: "" + property bool keyboardEnabled: HMD.active + property bool keyboardRaised: false + + anchors.fill: (typeof parent === undefined) ? undefined : parent function getMarketplaceItems() { marketplaceItemView.visible = false; @@ -44,11 +47,11 @@ Rectangle { } Component.onDestruction: { - KeyboardScriptingInterface.raised = false; + keyboard.raised = false; } Connections { - target: MarketplaceScriptingInterface; + target: MarketplaceScriptingInterface onGetMarketplaceCategoriesResult: { if (result.status !== 'success') { @@ -66,7 +69,7 @@ Rectangle { }); }); } - getMarketplaceItems(); + getMarketplaceItems(); } onGetMarketplaceItemsResult: { marketBrowseModel.handlePage(result.status !== "success" && result.message, result); @@ -77,7 +80,6 @@ Rectangle { console.log("Failed to get Marketplace Item", result.data.message); } else { - console.log(JSON.stringify(result.data)); marketplaceItem.item_id = result.data.id; marketplaceItem.image_url = result.data.thumbnail_url; marketplaceItem.name = result.data.title; @@ -91,7 +93,6 @@ Rectangle { marketplaceItem.license = result.data.license; marketplaceItem.available = result.data.availability == "available"; marketplaceItem.created_at = result.data.created_at; - console.log("HEIGHT: " + marketplaceItemContent.height); marketplaceItemScrollView.contentHeight = marketplaceItemContent.height; itemsList.visible = false; marketplaceItemView.visible = true; @@ -100,196 +101,194 @@ Rectangle { } HifiCommerceCommon.CommerceLightbox { - id: lightboxPopup; - visible: false; - anchors.fill: parent; + id: lightboxPopup + visible: false + anchors.fill: parent } // // HEADER BAR START // - Rectangle { - id: headerShadowBase; - anchors.fill: header; - color: "white"; + id: headerShadowBase + anchors.fill: header + color: "white" } DropShadow { - anchors.fill: headerShadowBase; - source: headerShadowBase; - verticalOffset: 4; - horizontalOffset: 4; - radius: 6; - samples: 9; - color: hifi.colors.baseGrayShadow; - z:5; - } + anchors.fill: headerShadowBase + source: headerShadowBase + verticalOffset: 4 + horizontalOffset: 4 + radius: 6 + samples: 9 + color: hifi.colors.baseGrayShadow + z:5 + } Rectangle { id: header; - visible: true; - anchors.left: parent.left; - anchors.top: parent.top; - anchors.right: parent.right; - anchors.topMargin: -1; - anchors.leftMargin: -1; - anchors.rightMargin: -1; - height: childrenRect.height+5; - z: 5; + + visible: true + anchors { + left: parent.left + top: parent.top + right: parent.right + } + + height: childrenRect.height+5 + z: 5 Rectangle { - id: titleBarContainer; - visible: true; - // Size - width: parent.width; - height: 50; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - - // Marketplace icon + id: titleBarContainer + + anchors.left: parent.left + anchors.top: parent.top + width: parent.width + height: 50 + visible: true + Image { - id: marketplaceIcon; - source: "../../../../images/hifi-logo-blackish.svg"; + id: marketplaceIcon + + anchors { + left: parent.left + leftMargin: 8 + verticalCenter: parent.verticalCenter + } height: 20 - width: marketplaceIcon.height; - anchors.left: parent.left; - anchors.leftMargin: 8; - anchors.verticalCenter: parent.verticalCenter; - visible: true; + width: marketplaceIcon.height + source: "../../../../images/hifi-logo-blackish.svg" + visible: true } - // Title Bar text RalewaySemiBold { - id: titleBarText; - text: "Marketplace"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: marketplaceIcon.right; - anchors.leftMargin: 6; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.black; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: titleBarText + + anchors { + top: parent.top + left: marketplaceIcon.right + bottom: parent.bottom + leftMargin: 6 + } + width: paintedWidth + + text: "Marketplace" + size: hifi.fontSizes.overlayTitle + color: hifi.colors.black + verticalAlignment: Text.AlignVCenter } } Rectangle { - id: searchBarContainer; - visible: true; - clip: false; - // Size - width: parent.width; - anchors.top: titleBarContainer.bottom; - height: 50; + id: searchBarContainer + anchors.top: titleBarContainer.bottom + width: parent.width + height: 50 + + visible: true + clip: false - Rectangle { - id: categoriesButton; - anchors.left: parent.left; - anchors.leftMargin: 10; - anchors.verticalCenter: parent.verticalCenter; - height: 34; - width: categoriesText.width + 25; - color: hifi.colors.white; - radius: 4; - border.width: 1; - border.color: hifi.colors.lightGrayText; - + // + // TODO: Possibly change this to be a combo box + // + Rectangle { + id: categoriesButton - // Categories Text - RalewayRegular { - id: categoriesText; - text: "Categories"; - // Text size - size: 14; - // Style - color: hifi.colors.lightGrayText; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - width: Math.min(textMetrics.width + 25, 110); - // Anchors - anchors.centerIn: parent; - rightPadding: 10; + anchors { + left: parent.left + leftMargin: 10 + verticalCenter: parent.verticalCenter } + height: 34 + width: categoriesText.width + 25 + + color: hifi.colors.white + radius: 4 + border.width: 1 + border.color: hifi.colors.lightGrayText + + RalewayRegular { + id: categoriesText + + anchors.centerIn: parent + + text: "Categories" + size: 14 + color: hifi.colors.lightGrayText + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + rightPadding: 10 + } + HiFiGlyphs { - id: categoriesDropdownIcon; - text: hifi.glyphs.caratDn; - // Size - size: 34; - // Anchors - anchors.right: parent.right; - anchors.rightMargin: -8; - anchors.verticalCenter: parent.verticalCenter; - horizontalAlignment: Text.AlignHCenter; - // Style - color: hifi.colors.baseGray; + id: categoriesDropdownIcon + + anchors { + right: parent.right + rightMargin: -8 + verticalCenter: parent.verticalCenter + } + + text: hifi.glyphs.caratDn + size: 34 + horizontalAlignment: Text.AlignHCenter + color: hifi.colors.baseGray } MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; + anchors.fill: parent + + hoverEnabled: enabled onClicked: { categoriesDropdown.visible = !categoriesDropdown.visible; categoriesButton.color = categoriesDropdown.visible ? hifi.colors.lightGray : hifi.colors.white; categoriesDropdown.forceActiveFocus(); } - onEntered: categoriesText.color = hifi.colors.baseGrayShadow; - onExited: categoriesText.color = hifi.colors.baseGray; + onEntered: categoriesText.color = hifi.colors.baseGrayShadow + onExited: categoriesText.color = hifi.colors.baseGray } Component.onCompleted: { - console.log("Getting Marketplace Categories"); MarketplaceScriptingInterface.getMarketplaceCategories(); } } // or RalewayRegular { - id: orText; - text: "or"; - // Text size + id: orText + + anchors.left: categoriesButton.right + anchors.verticalCenter: parent.verticalCenter + width: 25 + + text: "or" size: 18; - // Style - color: hifi.colors.baseGray; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - width: Math.min(textMetrics.width + 25, 110); - // Anchors - anchors.left: categoriesButton.right; - rightPadding: 10; - leftPadding: 10; - anchors.verticalCenter: parent.verticalCenter; + color: hifi.colors.baseGray + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + rightPadding: 10 + leftPadding: 10 } + HifiControlsUit.TextField { - id: searchField; - anchors.verticalCenter: parent.verticalCenter; - anchors.right: parent.right; - anchors.left: orText.right; - anchors.rightMargin: 10; - height: 34; - isSearchField: true; - colorScheme: hifi.colorSchemes.faintGray; - + id: searchField - font.family: "Fira Sans" - font.pixelSize: hifi.fontSizes.textFieldInput; - - placeholderText: "Search Marketplace"; - - TextMetrics { - id: primaryFilterTextMetrics; - font.family: "FiraSans Regular"; - font.pixelSize: hifi.fontSizes.textFieldInput; - font.capitalization: Font.AllUppercase; + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + left: orText.right + rightMargin: 10 } + height: 34 + + isSearchField: true + colorScheme: hifi.colorSchemes.faintGray + font.family: "Fira Sans" + font.pixelSize: hifi.fontSizes.textFieldInput + placeholderText: "Search Marketplace" // workaround for https://bugreports.qt.io/browse/QTBUG-49297 Keys.onPressed: { @@ -311,7 +310,7 @@ Rectangle { break; } } - onTextChanged: root.searchString = text; + onTextChanged: root.searchString = text onAccepted: { root.searchString = searchField.text; getMarketplaceItems(); @@ -319,15 +318,12 @@ Rectangle { } onActiveFocusChanged: { - if (!activeFocus) { - dropdownContainer.visible = false; - } + root.keyboardRaised = activeFocus; } } } } - // // HEADER BAR END // @@ -336,14 +332,15 @@ Rectangle { // CATEGORIES LIST START // Item { - id: categoriesDropdown; + id: categoriesDropdown + anchors.fill: parent; - visible: false; - z: 10; - + + visible: false + z: 10 MouseArea { - anchors.fill: parent; - propagateComposedEvents: true; + anchors.fill: parent + propagateComposedEvents: true onClicked: { categoriesDropdown.visible = false; categoriesButton.color = hifi.colors.white; @@ -351,57 +348,72 @@ Rectangle { } Rectangle { - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.top: parent.top; - anchors.topMargin: 100; - width: parent.width/3; - color: hifi.colors.white; + anchors { + left: parent.left; + bottom: parent.bottom; + top: parent.top; + topMargin: 100; + } + width: parent.width/3 + + color: hifi.colors.white ListModel { - id: categoriesModel; + id: categoriesModel } ListView { id: categoriesListView; - anchors.fill: parent; - anchors.rightMargin: 10; - width: parent.width; - clip: true; + + anchors.fill: parent + anchors.rightMargin: 10 + width: parent.width + + clip: true - model: categoriesModel; + model: categoriesModel delegate: ItemDelegate { - height: 34; - width: parent.width; - clip: true; + height: 34 + width: parent.width + + clip: true contentItem: Rectangle { - id: categoriesItem; - anchors.fill: parent; - color: hifi.colors.white; - visible: true; + id: categoriesItem + + anchors.fill: parent + + color: hifi.colors.white + visible: true RalewayRegular { - id: categoriesItemText; - text: model.name; - anchors.leftMargin: 15; - anchors.fill:parent; - color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 14; + id: categoriesItemText + + anchors.leftMargin: 15 + anchors.fill:parent + + text: model.name + color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 14 } } + MouseArea { - anchors.fill: parent; - hoverEnabled: true; - propagateComposedEvents: false; - z: 10; + anchors.fill: parent + z: 10 + + hoverEnabled: true + propagateComposedEvents: false + onEntered: { categoriesItem.color = ListView.isCurrentItem ? hifi.colors.white : hifi.colors.lightBlueHighlight; } + onExited: { categoriesItem.color = ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.white; } + onClicked: { categoriesListView.currentIndex = index; categoriesText.text = categoriesItemText.text; @@ -413,12 +425,17 @@ Rectangle { } } + ScrollBar.vertical: ScrollBar { - parent: categoriesListView.parent; - anchors.top: categoriesListView.top; - anchors.bottom: categoriesListView.bottom; - anchors.left: categoriesListView.right; - contentItem.opacity: 1; + parent: categoriesListView.parent + + anchors { + top: categoriesListView.top; + bottom: categoriesListView.bottom; + left: categoriesListView.right; + } + + contentItem.opacity: 1 } } } @@ -431,19 +448,24 @@ Rectangle { // ITEMS LIST START // Item { - id: itemsList; - anchors.fill: parent; - anchors.topMargin: 100; - anchors.bottomMargin: 50; + id: itemsList + + anchors { + fill: parent + topMargin: 100 + bottomMargin: 50 + } + visible: true; HifiModels.PSFListModel { - id: marketBrowseModel; - itemsPerPage: 7; - listModelName: 'marketBrowse'; - listView: marketBrowseList; - getPage: function () { + id: marketBrowseModel + itemsPerPage: 7 + listModelName: 'marketBrowse' + listView: marketBrowseList + + getPage: function () { MarketplaceScriptingInterface.getMarketplaceItems( root.searchString == "" ? undefined : root.searchString, "", @@ -456,90 +478,98 @@ Rectangle { marketBrowseModel.itemsPerPage ); } + processPage: function(data) { - console.log(JSON.stringify(data)); - data.items.forEach(function (item) { - console.log(JSON.stringify(item)); - }); return data.items; } } ListView { - id: marketBrowseList; - model: marketBrowseModel; - // Anchors - anchors.fill: parent; - anchors.rightMargin: 10; - orientation: ListView.Vertical; - focus: true; - clip: true; + id: marketBrowseList + + anchors.fill: parent + anchors.rightMargin: 10 + + model: marketBrowseModel + + orientation: ListView.Vertical + focus: true + clip: true delegate: MarketplaceListItem { - item_id: model.id; - image_url:model.thumbnail_url; - name: model.title; - likes: model.likes; - liked: model.has_liked; - creator: model.creator; - category: model.primary_category; - price: model.cost; - available: model.availability == "available"; - anchors.topMargin: 10; - - - Component.onCompleted: { - console.log("Rendering marketplace list item " + model.id); - console.log(marketBrowseModel.count); - } - + item_id: model.id + + anchors.topMargin: 10 + + image_url:model.thumbnail_url + name: model.title + likes: model.likes + liked: model.has_liked + creator: model.creator + category: model.primary_category + price: model.cost + available: model.availability == "available" + onShowItem: { MarketplaceScriptingInterface.getMarketplaceItem(item_id); } - + onBuy: { sendToScript({method: 'marketplace_checkout', itemId: item_id}); } - + onCategoryClicked: { root.categoryString = category; categoriesText.text = category; getMarketplaceItems(); } - + onRequestReload: getMarketplaceItems(); } + ScrollBar.vertical: ScrollBar { - parent: marketBrowseList.parent; - anchors.top: marketBrowseList.top; - anchors.bottom: marketBrowseList.bottom; - anchors.left: marketBrowseList.right; - contentItem.opacity: 1; - } - headerPositioning: ListView.InlineHeader; + parent: marketBrowseList.parent + + anchors { + top: marketBrowseList.top + bottom: marketBrowseList.bottom + left: marketBrowseList.right + } + + contentItem.opacity: 1 + } + + headerPositioning: ListView.InlineHeader + header: Item { - id: itemsHeading; + id: itemsHeading - height: childrenRect.height; - width: parent.width; + height: childrenRect.height + width: parent.width Item { - id: breadcrumbs; - anchors.left: parent.left; - anchors.right: parent.right; - height: 34; - visible: categoriesListView.currentIndex >= 0; + id: breadcrumbs + + anchors.left: parent.left + anchors.right: parent.right + height: 34 + visible: categoriesListView.currentIndex >= 0 + RalewayRegular { - id: categoriesItemText; - text: "Home /"; - anchors.leftMargin: 15; - anchors.fill:parent; - color: hifi.colors.blueHighlight; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 18; + id: categoriesItemText + + anchors.leftMargin: 15 + anchors.fill:parent + + text: "Home /" + color: hifi.colors.blueHighlight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 } + MouseArea { - anchors.fill: parent; + anchors.fill: parent + onClicked: { categoriesListView.currentIndex = -1; categoriesText.text = "Categories"; @@ -550,72 +580,91 @@ Rectangle { } Item { - id: searchScope; - anchors.top: breadcrumbs.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; + id: searchScope + + anchors { + top: breadcrumbs.bottom + left: parent.left + right: parent.right + } + height: 50 RalewaySemiBold { id: searchScopeText; + + anchors { + leftMargin: 15 + fill:parent + topMargin: 10 + } + text: "Featured"; - anchors.leftMargin: 15; - anchors.fill:parent; - anchors.topMargin: 10; - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 22; + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 22 } } Item { - id: sort; - anchors.top: searchScope.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.topMargin: 10; - anchors.leftMargin: 15; - height: childrenRect.height; + id: sort + + anchors { + top: searchScope.bottom; + left: parent.left; + right: parent.right; + topMargin: 10; + leftMargin: 15; + } + height: childrenRect.height + RalewayRegular { - id: sortText; - text: "Sort:"; - anchors.leftMargin: 15; - anchors.rightMargin: 20; - height: 34; - color: hifi.colors.lightGrayText; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 14; + id: sortText + + anchors.leftMargin: 15 + anchors.rightMargin: 20 + height: 34 + + text: "Sort:" + color: hifi.colors.lightGrayText + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 14 } Rectangle { - radius: 4; - border.width: 1; - border.color: hifi.colors.faintGray; - anchors.left: sortText.right; - anchors.top: parent.top; - width: 322; - height: 36; - anchors.leftMargin: 20; - - + anchors { + left: sortText.right + top: parent.top + leftMargin: 20 + } + width: 322 + height: 36 + + radius: 4 + border.width: 1 + border.color: hifi.colors.faintGray + ListModel { - id: sortModel; + id: sortModel + ListElement { name: "Name"; - glyph: ";"; - sortString: "alpha"; + glyph: ";" + sortString: "alpha" } + ListElement { name: "Date"; glyph: ";"; sortString: "recent"; } + ListElement { name: "Popular"; glyph: ";"; sortString: "likes"; } + ListElement { name: "My Likes"; glyph: ";"; @@ -624,30 +673,34 @@ Rectangle { } ListView { - id: sortListView; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.topMargin:1; - anchors.bottomMargin:1; - anchors.leftMargin:1; - anchors.rightMargin:1; - width: childrenRect.width; - height: 34; - orientation: ListView.Horizontal; - focus: true; - clip: true; - highlightFollowsCurrentItem: false; + id: sortListView + + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + topMargin:1 + bottomMargin:1 + leftMargin:1 + rightMargin:1 + } + width: childrenRect.width + height: 34 + + orientation: ListView.Horizontal + focus: true + clip: true + highlightFollowsCurrentItem: false delegate: SortButton { - width: 80; - height: parent.height; - glyph: model.glyph; - text: model.name; + width: 80 + height: parent.height + + glyph: model.glyph + text: model.name - checked: { - ListView.isCurrentItem; - } + checked: ListView.isCurrentItem + onClicked: { root.currentSortIndex = index; sortListView.positionViewAtIndex(index, ListView.Beginning); @@ -657,12 +710,14 @@ Rectangle { } } highlight: Rectangle { - width: 80; - height: parent.height; - color: hifi.colors.faintGray; - x: sortListView.currentItem.x; + width: 80 + height: parent.height + + color: hifi.colors.faintGray + x: sortListView.currentItem.x } - model: sortModel; + + model: sortModel } } } @@ -678,57 +733,62 @@ Rectangle { // ITEM START // Item { - id: marketplaceItemView; - anchors.fill: parent; - width: parent.width; - anchors.topMargin: 120; - visible: false; + id: marketplaceItemView + + anchors.fill: parent + anchors.topMargin: 120 + width: parent.width + + visible: false ScrollView { - id: marketplaceItemScrollView; + id: marketplaceItemScrollView + anchors.fill: parent; - clip: true; - ScrollBar.vertical.policy: ScrollBar.AlwaysOn; - contentWidth: parent.width; + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + contentWidth: parent.width Rectangle { - id: marketplaceItemContent; - width: parent.width; - height: childrenRect.height + 100; + id: marketplaceItemContent + + width: parent.width + height: childrenRect.height + 100 - // Title Bar text RalewaySemiBold { - id: backText; - text: "Back"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left - anchors.leftMargin: 15; - anchors.bottomMargin: 10; - width: paintedWidth; - height: paintedHeight; - // Style - color: hifi.colors.blueHighlight; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: backText + + anchors { + top: parent.top + left: parent.left + leftMargin: 15 + bottomMargin: 10 + } + width: paintedWidth + height: paintedHeight + + text: "Back" + size: hifi.fontSizes.overlayTitle + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter } + MouseArea { - anchors.fill: backText; + anchors.fill: backText onClicked: { getMarketplaceItems(); } } - + MarketplaceItem { - id: marketplaceItem; - anchors.top: backText.bottom; - width: parent.width; - height: childrenRect.height; - anchors.topMargin: 15; + id: marketplaceItem + + anchors.topMargin: 15 + anchors.top: backText.bottom + width: parent.width + height: childrenRect.height onBuy: { sendToScript({method: 'marketplace_checkout', itemId: item_id}); @@ -756,68 +816,77 @@ Rectangle { // Rectangle { - id: footer; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; + id: footer + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: 50 - color: hifi.colors.blueHighlight; + color: hifi.colors.blueHighlight Item { - anchors.fill: parent; - anchors.rightMargin: 15; - anchors.leftMargin: 15; - + anchors { + fill: parent + rightMargin: 15 + leftMargin: 15 + } + HiFiGlyphs { - id: footerGlyph; - text: hifi.glyphs.info; - // Size - size: 34; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - - anchors.rightMargin: 10; - // Style - color: hifi.colors.white; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + id: footerGlyph + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + rightMargin: 10 + } + + text: hifi.glyphs.info + size: 34 + color: hifi.colors.white + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } RalewaySemiBold { - id: footerInfo; - text: "Get items from Clara.io!"; - anchors.left: footerGlyph.right; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - color: hifi.colors.white; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - size: 18; + id: footerInfo + + anchors { + left: footerGlyph.right + top: parent.top + bottom: parent.bottom + } + + text: "Get items from Clara.io!" + color: hifi.colors.white + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 } HifiControlsUit.Button { - text: "SEE ALL MARKETS"; - anchors.right: parent.right; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.topMargin: 10; - anchors.bottomMargin: 10; - anchors.leftMargin: 10; - anchors.rightMargin: 10; - width: 180; - + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + topMargin: 10 + bottomMargin: 10 + leftMargin: 10 + rightMargin: 10 + } + + text: "SEE ALL MARKETS" + width: 180 + onClicked: { sendToScript({method: 'marketplace_marketplaces'}); } - } + } } } - - // // FOOTER END // @@ -827,46 +896,57 @@ Rectangle { // LICENSE START // Rectangle { - id: licenseInfo; - anchors.fill: root; - anchors.topMargin: 100; - anchors.bottomMargin: 0; - visible: false; - - - HifiControlsUit.WebView { - id: licenseInfoWebView; - anchors.bottomMargin: 1; - anchors.topMargin: 50; - anchors.leftMargin: 1; - anchors.rightMargin: 1; - anchors.fill: parent; + id: licenseInfo + + anchors { + fill: root + topMargin: 100 + bottomMargin: 0 } - Item { - id: licenseClose; - anchors.top: parent.top; - anchors.right: parent.right; - anchors.topMargin: 10; - anchors.rightMargin: 10; + + visible: false; + + HifiControlsUit.WebView { + id: licenseInfoWebView - width: 30; - height: 30; + anchors { + bottomMargin: 1 + topMargin: 50 + leftMargin: 1 + rightMargin: 1 + fill: parent + } + } + + Item { + id: licenseClose + + anchors { + top: parent.top + right: parent.right + topMargin: 10 + rightMargin: 10 + } + width: 30 + height: 30 + HiFiGlyphs { - anchors.fill: parent; - height: 30; - text: hifi.glyphs.close; - // Size - size: 34; - // Anchors - anchors.rightMargin: 0; - anchors.verticalCenter: parent.verticalCenter; - horizontalAlignment: Text.AlignHCenter; - // Style - color: hifi.colors.baseGray; + anchors { + fill: parent + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + height: 30 + + text: hifi.glyphs.close + size: 34 + horizontalAlignment: Text.AlignHCenter + color: hifi.colors.baseGray } MouseArea { - anchors.fill: licenseClose; + anchors.fill: licenseClose + onClicked: licenseInfo.visible = false; } } @@ -875,6 +955,19 @@ Rectangle { // LICENSE END // + HifiControlsUit.Keyboard { + id: keyboard + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: false + } + // // Function Name: fromScript() // @@ -890,7 +983,6 @@ Rectangle { // function fromScript(message) { - console.log("FROM SCRIPT " + JSON.stringify(message)); switch (message.method) { case 'updateMarketplaceQMLItem': if (!message.params.itemId) { diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index a7f2991920..9b95b36918 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -25,37 +25,39 @@ import "../.." as HifiCommon Rectangle { id: root; - property string item_id: ""; - property string image_url: ""; - property string name: ""; - property int likes: 0; - property bool liked: false; - property string creator: ""; - property var categories: []; - property int price: 0; - property var attributions: []; - property string description: ""; - property string license: ""; - property string posted: ""; - property bool available: false; - property string created_at: ""; + + property string item_id: "" + property string image_url: "" + property string name: "" + property int likes: 0 + property bool liked: false + property string creator: "" + property var categories: [] + property int price: 0 + property var attributions: [] + property string description: "" + property string license: "" + property string posted: "" + property bool available: false + property string created_at: "" onCategoriesChanged: { categoriesListModel.clear(); categories.forEach(function(category) { - console.log("category is " + category); categoriesListModel.append({"category":category}); }); } - signal buy(); - signal categoryClicked(string category); - signal showLicense(string url); + signal buy() + signal categoryClicked(string category) + signal showLicense(string url) - HifiConstants { id: hifi; } + HifiConstants { + id: hifi + } Connections { - target: MarketplaceScriptingInterface; + target: MarketplaceScriptingInterface onMarketplaceItemLikeResult: { if (result.status !== 'success') { @@ -95,80 +97,91 @@ Rectangle { return a.toDateString() + " " + drawnHour + ':' + min + amOrPm; } - anchors.left: parent.left; - anchors.right: parent.right; - anchors.leftMargin: 15; - anchors.rightMargin: 15; + anchors { + left: parent.left; + right: parent.right; + leftMargin: 15; + rightMargin: 15; + } height: childrenRect.height; - - + Rectangle { - id: header; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; + id: header + anchors { + left: parent.left; + right: parent.right; + top: parent.top; + } height: 50; RalewaySemiBold { - id: nameText; - text: root.name; - // Text size - size: 24; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.baseGray; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: nameText + + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + } + width: paintedWidth + + text: root.name + size: 24 + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter } + Item { - id: likes; - anchors.top: parent.top; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - anchors.rightMargin: 5; - + id: likes + + anchors { + top: parent.top; + right: parent.right; + bottom: parent.bottom; + rightMargin: 5; + } + RalewaySemiBold { - id: heart; - text: "\u2665"; - // Size - size: 20; - // Anchors - anchors.top: parent.top; - anchors.right: parent.right; - anchors.rightMargin: 0; - anchors.verticalCenter: parent.verticalCenter; - horizontalAlignment: Text.AlignHCenter; - // Style - color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText; + id: heart + + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + + text: "\u2665" + size: 20 + horizontalAlignment: Text.AlignHCenter + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText } RalewaySemiBold { - id: likesText; - text: root.likes; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.right: heart.left; - anchors.rightMargin: 5; - anchors.leftMargin: 15; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.baseGray; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: likesText + + anchors { + top: parent.top + right: heart.left + rightMargin: 5 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.likes + size: hifi.fontSizes.overlayTitle + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter } + MouseArea { - anchors.left: likesText.left; - anchors.right: heart.right; - anchors.top: likesText.top; - anchors.bottom: likesText.bottom; + anchors { + left: likesText.left + right: heart.right + top: likesText.top + bottom: likesText.bottom + } onClicked: { MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, !root.liked); @@ -176,178 +189,195 @@ Rectangle { } } } + Image { - id: itemImage; - source: root.image_url; - anchors.top: header.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + id: itemImage + + anchors { + top: header.bottom + left: parent.left + right: parent.right + } height: width*0.5625 + + source: root.image_url fillMode: Image.PreserveAspectCrop; } + Item { - id: footer; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: itemImage.bottom; - height: childrenRect.height; + id: footer + + anchors { + left: parent.left; + right: parent.right; + top: itemImage.bottom; + } + height: childrenRect.height HifiControlsUit.Button { - id: buyButton; - text: root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)"; - enabled: root.available; - buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : ""; - anchors.right: parent.right; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.topMargin: 15; - height: 50; - color: hifi.buttons.blue; + id: buyButton + + anchors { + right: parent.right + top: parent.top + left: parent.left + topMargin: 15 + } + height: 50 + + text: root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)" + enabled: root.available + buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : "" + color: hifi.buttons.blue onClicked: root.buy(); } Item { - id: creatorItem; - anchors.top: buyButton.bottom; - anchors.leftMargin: 15; - anchors.topMargin: 15; - width: parent.width; - height: childrenRect.height; + id: creatorItem + + anchors { + top: buyButton.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: childrenRect.height RalewaySemiBold { - id: creatorLabel; - text: "CREATOR:"; - // Text size - size: 14; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: creatorLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "CREATOR:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter } RalewaySemiBold { - id: creatorText; - text: root.creator; - // Text size - size: 18; - // Anchors - anchors.top: creatorLabel.bottom; - anchors.left: parent.left; - anchors.topMargin: 10; - width: paintedWidth; - // Style - color: hifi.colors.blueHighlight; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: creatorText + + anchors { + top: creatorLabel.bottom + left: parent.left + topMargin: 10 + } + width: paintedWidth + text: root.creator + + size: 18 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter } } Item { - id: posted; - anchors.top: creatorItem.bottom; - anchors.leftMargin: 15; - anchors.topMargin: 15; - width: parent.width; - height: childrenRect.height; - RalewaySemiBold { - id: postedLabel; - text: "POSTED:"; - // Text size - size: 14; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; + id: posted + + anchors { + top: creatorItem.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: childrenRect.height - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - verticalAlignment: Text.AlignVCenter; + RalewaySemiBold { + id: postedLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "POSTED:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter } RalewaySemiBold { - id: created_at; + id: created_at + + anchors { + top: postedLabel.bottom + left: parent.left + right: parent.right + topMargin: 10 + } + text: { getFormattedDate(root.created_at); } - // Text size - size: 14; - // Anchors - anchors.top: postedLabel.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.topMargin: 10; - // Style - color: hifi.colors.lightGray; - // Alignment - verticalAlignment: Text.AlignVCenter; + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter } } Rectangle { - anchors.top: posted.bottom; - anchors.leftMargin: 15; - anchors.topMargin: 15; + anchors { + top: posted.bottom; + leftMargin: 15; + topMargin: 15; + } width: parent.width; height: 1; + color: hifi.colors.lightGray; } Item { id: licenseItem; - anchors.top: posted.bottom; - anchors.left: parent.left; - anchors.topMargin: 30; - width: parent.width; - height: childrenRect.height; + + anchors { + top: posted.bottom + left: parent.left + topMargin: 30 + } + width: parent.width + height: childrenRect.height + RalewaySemiBold { - id: licenseLabel; - text: "SHARED UNDER:"; - // Text size - size: 14; - // Anchors + id: licenseLabel + anchors.top: parent.top; anchors.left: parent.left; width: paintedWidth; - // Style + + text: "SHARED UNDER:"; + size: 14; color: hifi.colors.lightGrayText; - // Alignment verticalAlignment: Text.AlignVCenter; } - + RalewaySemiBold { - id: licenseText; - text: root.license; - // Text size - size: 14; - // Anchors - anchors.top: licenseLabel.bottom; - anchors.left: parent.left; - width: paintedWidth; - // Style - color: hifi.colors.lightGray; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: licenseText + + anchors.top: licenseLabel.bottom + anchors.left: parent.left + width: paintedWidth + + text: root.license + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter } + RalewaySemiBold { - id: licenseHelp; - text: "More about this license"; - // Text size - size: 14; - // Anchors + id: licenseHelp + anchors.top: licenseText.bottom; anchors.left: parent.left; width: paintedWidth; - // Style - color: hifi.colors.blueHighlight; - // Alignment - verticalAlignment: Text.AlignVCenter; + + text: "More about this license" + size: 14 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter MouseArea { - anchors.fill: parent; + anchors.fill: parent onClicked: { licenseInfo.visible = true; @@ -371,7 +401,6 @@ Rectangle { } if(url) { licenseInfoWebView.url = url; - } } } @@ -379,91 +408,97 @@ Rectangle { } Item { - id: descriptionItem; - anchors.top: licenseItem.bottom; - anchors.topMargin: 15; - anchors.left: parent.left; - anchors.right: parent.right; - height: childrenRect.height; - RalewaySemiBold { - id: descriptionLabel; - text: "DESCRIPTION:"; - // Text size - size: 14; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: descriptionItem + + anchors { + top: licenseItem.bottom + topMargin: 15 + left: parent.left + right: parent.right } + height: childrenRect.height + RalewaySemiBold { - id: descriptionText; - text: root.description; - // Text size - size: 14; - // Anchors - anchors.top: descriptionLabel.bottom; - anchors.left: parent.left; - width: parent.width; - // Style - color: hifi.colors.lightGray; - // Alignment - verticalAlignment: Text.AlignVCenter; - wrapMode: Text.Wrap; + id: descriptionLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "DESCRIPTION:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: descriptionText + + anchors.top: descriptionLabel.bottom + anchors.left: parent.left + width: parent.width + + text: root.description + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap } } Item { - id: categoriesList; - anchors.top: descriptionItem.bottom; - anchors.topMargin: 15; - anchors.left: parent.left; - anchors.right: parent.right; - width: parent.width; - height: childrenRect.height; + id: categoriesList + + anchors { + top: descriptionItem.bottom + topMargin: 15 + left: parent.left + right: parent.right + } + width: parent.width + height: childrenRect.height + RalewaySemiBold { - id: categoryLabel; - text: "IN:"; - // Text size - size: 14; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: categoryLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "IN:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter } ListModel { - id: categoriesListModel; + id: categoriesListModel } ListView { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: categoryLabel.bottom; - model: categoriesListModel; - height: 20*model.count; + anchors { + left: parent.left; + right: parent.right; + top: categoryLabel.bottom; + } + + height: 20*model.count + + model: categoriesListModel delegate: RalewaySemiBold { - id: categoryText; - text: model.category; - // Text size - size: 14; - // Anchors - anchors.leftMargin: 15; - width: paintedWidth; - height: 20; - // Style - color: hifi.colors.blueHighlight; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: categoryText + + anchors.leftMargin: 15 + width: paintedWidth + + text: model.category + size: 14 + height: 20 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter MouseArea { - anchors.fill: categoryText; + anchors.fill: categoryText + onClicked: root.categoryClicked(model.category); } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml index 4b8471e8b9..46e6a9d920 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -24,98 +24,115 @@ import "../common/sendAsset" import "../.." as HifiCommon Rectangle { - id: root; - property string item_id: ""; - property string image_url: ""; - property string name: ""; - property int likes: 0; - property bool liked: false; - property string creator: ""; - property string category: ""; - property int price: 0; - property bool available: false; + id: root - signal buy(); - signal showItem(); - signal categoryClicked(string category); - signal requestReload(); + property string item_id: "" + property string image_url: "" + property string name: "" + property int likes: 0 + property bool liked: false + property string creator: "" + property string category: "" + property int price: 0 + property bool available: false + + signal buy() + signal showItem() + signal categoryClicked(string category) + signal requestReload() - HifiConstants { id: hifi; } + HifiConstants { id: hifi } - width: parent.width; - height: childrenRect.height+50; + width: parent.width + height: childrenRect.height+50 DropShadow { - anchors.fill: shadowBase; - source: shadowBase; - verticalOffset: 4; - horizontalOffset: 4; - radius: 6; - samples: 9; - color: hifi.colors.baseGrayShadow; + anchors.fill: shadowBase + + source: shadowBase + verticalOffset: 4 + horizontalOffset: 4 + radius: 6 + samples: 9 + color: hifi.colors.baseGrayShadow } Rectangle { - id: shadowBase; - anchors.fill: itemRect; - color: "white"; + id: shadowBase + + anchors.fill: itemRect + + color: "white" } Rectangle { - id: itemRect; - height: childrenRect.height; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - anchors.topMargin: 20; - anchors.bottomMargin: 10; - anchors.leftMargin: 15; - anchors.rightMargin: 15; + id: itemRect + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 20 + bottomMargin: 10 + leftMargin: 15 + rightMargin: 15 + } + height: childrenRect.height + MouseArea { - anchors.fill: parent; + anchors.fill: parent + + hoverEnabled: true + onClicked: root.showItem(); - onEntered: { hoverRect.visible = true; console.log("entered"); } - onExited: { hoverRect.visible = false; console.log("exited"); } - hoverEnabled: true + onEntered: hoverRect.visible = true; + onExited: hoverRect.visible = false; + } Rectangle { - id: header; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - - height: 50; + id: header + + anchors { + left: parent.left + right: parent.right + top: parent.top + } + height: 50 RalewaySemiBold { - id: nameText; - text: root.name; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.rightMargin: 50; - anchors.leftMargin: 15; - anchors.bottom: parent.bottom; - elide: Text.ElideRight; - width: paintedWidth; - // Style - color: hifi.colors.blueHighlight; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: nameText + + anchors { + top: parent.top + left: parent.left + right: parent.right + rightMargin: 50 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.name + size: hifi.fontSizes.overlayTitle + elide: Text.ElideRight + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter } + Item { - id: likes; - anchors.top: parent.top; - anchors.right: parent.right; - anchors.rightMargin: 15; - anchors.bottom: parent.bottom; - width: childrenRect.width; + id: likes + + anchors { + top: parent.top + right: parent.right + rightMargin: 15 + bottom: parent.bottom + } + width: heart.width + likesText.width + Connections { - target: MarketplaceScriptingInterface; + target: MarketplaceScriptingInterface onMarketplaceItemLikeResult: { if (result.status !== 'success') { @@ -126,41 +143,47 @@ Rectangle { } RalewaySemiBold { - id: heart; - text: "\u2665"; - // Size - size: 20; - // Anchors - anchors.top: parent.top; - anchors.right: parent.right; - anchors.rightMargin: 0; - anchors.verticalCenter: parent.verticalCenter; + id: heart + + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + + text: "\u2665" + size: 20 horizontalAlignment: Text.AlignHCenter; - // Style - color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText; + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText } RalewaySemiBold { - id: likesText; - text: root.likes; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.top: parent.top; - anchors.right: heart.left; - anchors.rightMargin: 5; - anchors.leftMargin: 15; - anchors.bottom: parent.bottom; - width: paintedWidth; - // Style - color: hifi.colors.baseGray; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: likesText + + anchors { + top: parent.top + right: heart.left + rightMargin: 5 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.likes + size: hifi.fontSizes.overlayTitle + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter } + MouseArea { - anchors.fill: parent; + anchors { + left: likesText.left + right: heart.right + top: parent.top + bottom: parent.bottom + } onClicked: { - console.log("like " + root.item_id); root.liked = !root.liked; root.likes = root.liked ? root.likes + 1 : root.likes - 1; MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); @@ -168,111 +191,128 @@ Rectangle { } } } + Image { - id: itemImage; - source: root.image_url; - anchors.top: header.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + id: itemImage + + anchors { + top: header.bottom + left: parent.left + right: parent.right + } height: width*0.5625 - fillMode: Image.PreserveAspectCrop; + + source: root.image_url + fillMode: Image.PreserveAspectCrop } + Item { - id: footer; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: itemImage.bottom; - height: 60; + id: footer + + anchors { + left: parent.left + right: parent.right + top: itemImage.bottom + } + height: 60 RalewaySemiBold { - id: creatorLabel; - text: "CREATOR:"; - // Text size - size: 14; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 15; - width: paintedWidth; - // Style - color: hifi.colors.lightGrayText; - // Alignment - verticalAlignment: Text.AlignVCenter; + id: creatorLabel + + anchors { + top: parent.top + left: parent.left + leftMargin: 15 + } + width: paintedWidth + + text: "CREATOR:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter } RalewaySemiBold { - id: creatorText; + id: creatorText + + anchors { + top: creatorLabel.top; + left: creatorLabel.right; + leftMargin: 15; + } + width: paintedWidth; + text: root.creator; - // Text size size: 14; - // Anchors - anchors.top: creatorLabel.top; - anchors.left: creatorLabel.right; - anchors.leftMargin: 15; - width: paintedWidth; - // Style color: hifi.colors.lightGray; - // Alignment verticalAlignment: Text.AlignVCenter; } + RalewaySemiBold { - id: categoryLabel; + id: categoryLabel + + anchors { + top: creatorLabel.bottom + left: parent.left + leftMargin: 15 + } + width: paintedWidth; + text: "IN:"; - // Text size size: 14; - // Anchors - anchors.top: creatorLabel.bottom; - anchors.left: parent.left; - anchors.leftMargin: 15; - width: paintedWidth; - // Style color: hifi.colors.lightGrayText; - // Alignment verticalAlignment: Text.AlignVCenter; } - + RalewaySemiBold { - id: categoryText; - text: root.category; - // Text size - size: 14; - // Anchors - anchors.top: categoryLabel.top; - anchors.left: categoryLabel.right; - anchors.leftMargin: 15; - width: paintedWidth; - // Style + id: categoryText + + anchors { + top: categoryLabel.top + left: categoryLabel.right + leftMargin: 15 + } + width: paintedWidth + + text: root.category + size: 14 color: hifi.colors.blueHighlight; - // Alignment verticalAlignment: Text.AlignVCenter; MouseArea { - anchors.fill: parent; + anchors.fill: parent + onClicked: root.categoryClicked(root.category); } } HifiControlsUit.Button { - text: root.price ? root.price : "FREE"; - buttonGlyph: root.price ? hifi.glyphs.hfc : ""; - anchors.right: parent.right; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.rightMargin: 15; - anchors.topMargin:10; - anchors.bottomMargin: 10; + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + rightMargin: 15 + topMargin:10 + bottomMargin: 10 + } + + text: root.price ? root.price : "FREE" + buttonGlyph: root.price ? hifi.glyphs.hfc : "" color: hifi.buttons.blue; - + onClicked: root.buy(); } } + Rectangle { - id: hoverRect; - anchors.fill: parent; - border.color: hifi.colors.blueHighlight; - border.width: 2; - color: "#00000000"; - visible: false; + id: hoverRect + + anchors.fill: parent + + border.color: hifi.colors.blueHighlight + border.width: 2 + color: "#00000000" + visible: false } } } From 57d3cec2f942fba61806d6107293a8e6c0835a70 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 14:34:11 -0800 Subject: [PATCH 009/130] Completed - downloading Installer (APK) --- tools/nitpick/src/Nitpick.cpp | 3 ++- tools/nitpick/src/TestRunnerMobile.cpp | 9 ++++++++- tools/nitpick/src/TestRunnerMobile.h | 1 + tools/nitpick/ui/Nitpick.ui | 4 ++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 6db107b9a7..61e5d1d8d4 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -113,7 +113,8 @@ void Nitpick::setup() { _ui.folderLineEdit, _ui.downloadAPKPushbutton, _ui.runLatestOnMobileCheckBox, - _ui.urlOnMobileLineEdit + _ui.urlOnMobileLineEdit, + _ui.statusLabelOnMobile ); } diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 85aaeec609..31e293cd45 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -25,6 +25,7 @@ TestRunnerMobile::TestRunnerMobile( QPushButton* downloadAPKPushbutton, QCheckBox* runLatest, QLineEdit* url, + QLabel* statusLabel, QObject* parent ) : QObject(parent) @@ -37,6 +38,7 @@ TestRunnerMobile::TestRunnerMobile( _downloadAPKPushbutton = downloadAPKPushbutton; _runLatest = runLatest; _url = url; + _statusLabel = statusLabel; folderLineEdit->setText("/sdcard/DCIM/TEST"); } @@ -124,6 +126,11 @@ void TestRunnerMobile::downloadComplete() { _installerFilename = INSTALLER_FILENAME_LATEST; + + // Replace the `exe` extension with `apk` + _installerFilename = _installerFilename.replace(_installerFilename.length() - 3, 3, "apk"); + _buildInformation.url = _buildInformation.url.replace(_buildInformation.url.length() - 3, 3, "apk"); + urls << _buildInformation.url; filenames << _installerFilename; } else { @@ -135,7 +142,7 @@ void TestRunnerMobile::downloadComplete() { _statusLabel->setText("Downloading installer"); - //// nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); // `downloadComplete` will run again after download has completed diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 8d08eda191..65999439b9 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -29,6 +29,7 @@ public: QPushButton* downloadAPKPushbutton, QCheckBox* runLatest, QLineEdit* url, + QLabel* statusLabel, QObject* parent = 0 ); diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 50d6a1f661..480d0f87f5 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -630,8 +630,8 @@ 10 350 - 441 - 31 + 440 + 30 From dfac0d88a25abd56cde17679dc3f08df379790a6 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 25 Jan 2019 16:41:13 -0800 Subject: [PATCH 010/130] QmlMarketplace - Add 'logged out' behavior, fix issue with search scope display --- .../hifi/commerce/marketplace/Marketplace.qml | 178 ++++++++++++++++-- .../commerce/marketplace/MarketplaceItem.qml | 21 ++- .../marketplace/MarketplaceListItem.qml | 9 +- interface/src/Application.cpp | 1 + scripts/system/marketplaces/marketplaces.js | 2 +- 5 files changed, 184 insertions(+), 27 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 77820ccaca..10b59ac83b 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -37,18 +37,42 @@ Rectangle { property string searchString: "" property bool keyboardEnabled: HMD.active property bool keyboardRaised: false + property string searchScopeString: "Featured" + property bool isLoggedIn: false; anchors.fill: (typeof parent === undefined) ? undefined : parent function getMarketplaceItems() { marketplaceItemView.visible = false; - itemsList.visible = true; + itemsList.visible = true; marketBrowseModel.getFirstPage(); + { + if(root.searchString !== undefined && root.searchString !== "") { + root.searchScopeString = "Search Results: \"" + root.searchString + "\""; + } else if (root.categoryString !== "") { + root.searchScopeString = root.categoryString; + } else { + root.searchScopeString = "Featured"; + } + } } - + + Component.onCompleted: { + Commerce.getLoginStatus(); + } + Component.onDestruction: { keyboard.raised = false; } + + Connections { + target: GlobalServices + + onMyUsernameChanged: { + console.log("LOGIN STATUS CHANGING"); + Commerce.getLoginStatus(); + } + } Connections { target: MarketplaceScriptingInterface @@ -61,7 +85,7 @@ Rectangle { categoriesModel.append({ id: -1, name: "Everything" - }); + }); result.data.items.forEach(function(category) { categoriesModel.append({ id: category.id, @@ -84,14 +108,16 @@ Rectangle { marketplaceItem.image_url = result.data.thumbnail_url; marketplaceItem.name = result.data.title; marketplaceItem.likes = result.data.likes; - marketplaceItem.liked = result.data.has_liked; + if(result.data.has_liked !== undefined) { + marketplaceItem.liked = result.data.has_liked; + } marketplaceItem.creator = result.data.creator; marketplaceItem.categories = result.data.categories; marketplaceItem.price = result.data.cost; marketplaceItem.description = result.data.description; marketplaceItem.attributions = result.data.attributions; marketplaceItem.license = result.data.license; - marketplaceItem.available = result.data.availability == "available"; + marketplaceItem.available = result.data.availability === "available"; marketplaceItem.created_at = result.data.created_at; marketplaceItemScrollView.contentHeight = marketplaceItemContent.height; itemsList.visible = false; @@ -100,6 +126,15 @@ Rectangle { } } + Connections { + target: Commerce + + onLoginStatusResult: { + root.isLoggedIn = isLoggedIn; + itemsLoginStatus.visible = !isLoggedIn; + } + } + HifiCommerceCommon.CommerceLightbox { id: lightboxPopup visible: false @@ -423,7 +458,6 @@ Rectangle { getMarketplaceItems(); } } - } ScrollBar.vertical: ScrollBar { @@ -452,12 +486,12 @@ Rectangle { anchors { fill: parent - topMargin: 100 + topMargin: 120 bottomMargin: 50 } - + visible: true; - + HifiModels.PSFListModel { id: marketBrowseModel @@ -467,7 +501,7 @@ Rectangle { getPage: function () { MarketplaceScriptingInterface.getMarketplaceItems( - root.searchString == "" ? undefined : root.searchString, + root.searchString === "" ? undefined : root.searchString, "", root.categoryString.toLowerCase(), "", @@ -503,11 +537,12 @@ Rectangle { image_url:model.thumbnail_url name: model.title likes: model.likes - liked: model.has_liked + liked: model.has_liked ? model.has_liked : false creator: model.creator category: model.primary_category price: model.cost - available: model.availability == "available" + available: model.availability === "available" + isLoggedIn: root.isLoggedIn; onShowItem: { MarketplaceScriptingInterface.getMarketplaceItem(item_id); @@ -546,9 +581,65 @@ Rectangle { height: childrenRect.height width: parent.width + Rectangle { + id: itemsLoginStatus; + anchors { + left: parent.left + right: parent.right + leftMargin: 15 + rightMargin: 15 + } + height: root.isLoggedIn ? 0 : 80 + + visible: !root.isLoggedIn + color: hifi.colors.greenHighlight + border.color: hifi.colors.greenShadow + border.width: 1 + radius: 4 + z: 10000 + + HifiControlsUit.Button { + id: loginButton; + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + leftMargin: 15 + topMargin:10 + bottomMargin: 10 + } + width: 80; + + text: root.price ? root.price : "LOG IN" + + onClicked: { + sendToScript({method: 'needsLogIn_loginClicked'}); + } + } + + RalewayRegular { + id: itemsLoginText + + anchors { + leftMargin: 15 + top: parent.top; + bottom: parent.bottom; + right: parent.right; + left: loginButton.right + } + + text: "to get items from the Marketplace." + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + Item { id: breadcrumbs + anchors.top: itemsLoginStatus.bottom; anchors.left: parent.left anchors.right: parent.right height: 34 @@ -598,7 +689,7 @@ Rectangle { topMargin: 10 } - text: "Featured"; + text: searchScopeString color: hifi.colors.baseGray horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter @@ -751,8 +842,64 @@ Rectangle { contentWidth: parent.width Rectangle { - id: marketplaceItemContent + id: itemLoginStatus; + anchors { + left: parent.left + right: parent.right + leftMargin: 15 + rightMargin: 15 + } + height: root.isLoggedIn ? 0 : 80 + visible: !root.isLoggedIn + color: hifi.colors.greenHighlight + border.color: hifi.colors.greenShadow + border.width: 1 + radius: 4 + z: 10000 + + HifiControlsUit.Button { + id: loginButton; + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + leftMargin: 15 + topMargin:10 + bottomMargin: 10 + } + width: 80; + + text: root.price ? root.price : "LOG IN" + + onClicked: { + sendToScript({method: 'needsLogIn_loginClicked'}); + } + } + + RalewayRegular { + id: itemsLoginText + + anchors { + leftMargin: 15 + top: parent.top; + bottom: parent.bottom; + right: parent.right; + left: loginButton.right + } + + text: "to get items from the Marketplace." + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + + + Rectangle { + id: marketplaceItemContent + anchors.top: itemLoginStatus.bottom; width: parent.width height: childrenRect.height + 100 @@ -785,11 +932,14 @@ Rectangle { MarketplaceItem { id: marketplaceItem + anchors.topMargin: 15 anchors.top: backText.bottom width: parent.width height: childrenRect.height + isLoggedIn: root.isLoggedIn; + onBuy: { sendToScript({method: 'marketplace_checkout', itemId: item_id}); } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 9b95b36918..5795d0d67d 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -40,6 +40,7 @@ Rectangle { property string posted: "" property bool available: false property string created_at: "" + property bool isLoggedIn: false; onCategoriesChanged: { categoriesListModel.clear(); @@ -184,7 +185,9 @@ Rectangle { } onClicked: { - MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, !root.liked); + if (isLoggedIn) { + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, !root.liked); + } } } } @@ -382,21 +385,21 @@ Rectangle { onClicked: { licenseInfo.visible = true; var url; - if (root.license == "No Rights Reserved (CC0)") { + if (root.license === "No Rights Reserved (CC0)") { url = "https://creativecommons.org/publicdomain/zero/1.0/" - } else if (root.license == "Attribution (CC BY)") { + } else if (root.license === "Attribution (CC BY)") { url = "https://creativecommons.org/licenses/by/4.0/" - } else if (root.license == "Attribution-ShareAlike (CC BY-SA)") { + } else if (root.license === "Attribution-ShareAlike (CC BY-SA)") { url = "https://creativecommons.org/licenses/by-sa/4.0/" - } else if (root.license == "Attribution-NoDerivs (CC BY-ND)") { + } else if (root.license === "Attribution-NoDerivs (CC BY-ND)") { url = "https://creativecommons.org/licenses/by-nd/4.0/" - } else if (root.license == "Attribution-NonCommercial (CC BY-NC)") { + } else if (root.license === "Attribution-NonCommercial (CC BY-NC)") { url = "https://creativecommons.org/licenses/by-nc/4.0/" - } else if (root.license == "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)") { + } else if (root.license === "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)") { url = "https://creativecommons.org/licenses/by-nc-sa/4.0/" - } else if (root.license == "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { + } else if (root.license === "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" - } else if (root.license == "Proof of Provenance License (PoP License)") { + } else if (root.license === "Proof of Provenance License (PoP License)") { url = "https://digitalassetregistry.com/PoP-License/v1/" } if(url) { diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml index 46e6a9d920..eb99106cf4 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -35,6 +35,7 @@ Rectangle { property string category: "" property int price: 0 property bool available: false + property bool isLoggedIn: false; signal buy() signal showItem() @@ -184,9 +185,11 @@ Rectangle { bottom: parent.bottom } onClicked: { - root.liked = !root.liked; - root.likes = root.liked ? root.likes + 1 : root.likes - 1; - MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); + if(isLoggedIn) { + root.liked = !root.liked; + root.likes = root.liked ? root.likes + 1 : root.likes - 1; + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); + } } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2f0ca7ea2e..7ae7ebf65c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2990,6 +2990,7 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, QUrl{ "hifi/tablet/TabletMenu.qml" }, + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, }, commerceCallback); QmlContextCallback marketplaceCallback = [](QQmlContext* context) { diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 2ca79b49f0..655d286049 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -613,7 +613,7 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. - ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; case 'needsLogIn_loginClicked': openLoginWindow(); From ce90b62c3e557fc6fe06275ead9476c454388ee0 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 16:48:29 -0800 Subject: [PATCH 011/130] Testing APK installation. --- tools/nitpick/src/Nitpick.cpp | 5 ++++ tools/nitpick/src/Nitpick.h | 3 +++ tools/nitpick/src/TestRunner.cpp | 17 +++++++++++++ tools/nitpick/src/TestRunner.h | 9 +++++++ tools/nitpick/src/TestRunnerDesktop.cpp | 16 ------------ tools/nitpick/src/TestRunnerDesktop.h | 8 ------ tools/nitpick/src/TestRunnerMobile.cpp | 33 ++++++++++++------------- tools/nitpick/src/TestRunnerMobile.h | 8 +++++- tools/nitpick/ui/Nitpick.ui | 16 ++++++++++++ 9 files changed, 73 insertions(+), 42 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 61e5d1d8d4..9e385bcd4d 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -112,6 +112,7 @@ void Nitpick::setup() { _ui.detectedDeviceLabel, _ui.folderLineEdit, _ui.downloadAPKPushbutton, + _ui.installAPKPushbutton, _ui.runLatestOnMobileCheckBox, _ui.urlOnMobileLineEdit, _ui.statusLabelOnMobile @@ -362,6 +363,10 @@ void Nitpick::on_downloadAPKPushbutton_clicked() { _testRunnerMobile->downloadAPK(); } +void Nitpick::on_installAPKPushbutton_clicked() { + _testRunnerMobile->installAPK(); +} + void Nitpick::on_pullFolderPushbutton_clicked() { _testRunnerMobile->pullFolder(); } diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index f54754de2e..00516d1e76 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -98,7 +98,10 @@ private slots: void on_setWorkingFolderRunOnMobilePushbutton_clicked(); void on_connectDevicePushbutton_clicked(); void on_runLatestOnMobileCheckBox_clicked(); + void on_downloadAPKPushbutton_clicked(); + void on_installAPKPushbutton_clicked(); + void on_pullFolderPushbutton_clicked(); private: diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 6d3a3e1b84..54246de80b 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -32,6 +32,9 @@ void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { } workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); + + // This file is used for debug purposes. + _logFile.setFileName(_workingFolder + "/log.txt"); } void TestRunner::downloadBuildXml(void* caller) { @@ -151,6 +154,20 @@ QString TestRunner::getInstallerNameFromURL(const QString& url) { } } +void TestRunner::appendLog(const QString& message) { + if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open the log file"); + exit(-1); + } + + _logFile.write(message.toStdString().c_str()); + _logFile.write("\n"); + _logFile.close(); + + nitpick->appendLogWindow(message); +} + void Worker::setCommandLine(const QString& commandLine) { _commandLine = commandLine; } diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index 0453bcf491..d2468ec2fa 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -12,9 +12,11 @@ #define hifi_testRunner_h #include +#include #include #include #include +#include class Worker; @@ -31,6 +33,8 @@ public: void parseBuildInformation(); QString getInstallerNameFromURL(const QString& url); + void appendLog(const QString& message); + protected: QLabel* _workingFolderLabel; QLabel* _statusLabel; @@ -52,6 +56,11 @@ protected: #else const QString INSTALLER_FILENAME_LATEST{ "" }; #endif + + QDateTime _testStartDateTime; + +private: + QFile _logFile; }; class Worker : public QObject { diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp index eb371f7e85..50cb6f9a07 100644 --- a/tools/nitpick/src/TestRunnerDesktop.cpp +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -83,8 +83,6 @@ void TestRunnerDesktop::setWorkingFolderAndEnableControls() { _installationFolder = _workingFolder + "/High_Fidelity"; #endif - _logFile.setFileName(_workingFolder + "/log.txt"); - nitpick->enableRunTabControls(); _timer = new QTimer(this); @@ -661,20 +659,6 @@ void TestRunnerDesktop::checkTime() { } } -void TestRunnerDesktop::appendLog(const QString& message) { - if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open the log file"); - exit(-1); - } - - _logFile.write(message.toStdString().c_str()); - _logFile.write("\n"); - _logFile.close(); - - nitpick->appendLogWindow(message); -} - QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { try { QStringList urlParts = url.split("/"); diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h index 83551cef0e..a8f828b9d4 100644 --- a/tools/nitpick/src/TestRunnerDesktop.h +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -16,7 +16,6 @@ #include #include #include -#include #include #include "TestRunner.h" @@ -67,8 +66,6 @@ public: void copyFolder(const QString& source, const QString& destination); - void appendLog(const QString& message); - QString getPRNumberFromURL(const QString& url); private slots: @@ -106,11 +103,6 @@ private: QCheckBox* _runServerless; QPushButton* _runNow; QTimer* _timer; - - QFile _logFile; - - QDateTime _testStartDateTime; - QThread* _installerThread; QThread* _interfaceThread; diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 31e293cd45..480381dd8e 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -23,6 +23,7 @@ TestRunnerMobile::TestRunnerMobile( QLabel* detectedDeviceLabel, QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, QCheckBox* runLatest, QLineEdit* url, QLabel* statusLabel, @@ -36,6 +37,7 @@ TestRunnerMobile::TestRunnerMobile( _detectedDeviceLabel = detectedDeviceLabel; _folderLineEdit = folderLineEdit; _downloadAPKPushbutton = downloadAPKPushbutton; + _installAPKPushbutton = installAPKPushbutton; _runLatest = runLatest; _url = url; _statusLabel = statusLabel; @@ -50,7 +52,6 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { setWorkingFolder(_workingFolderLabel); _connectDeviceButton->setEnabled(true); - _downloadAPKPushbutton->setEnabled(true); // Find ADB (Android Debugging Bridge) before continuing #ifdef Q_OS_WIN @@ -104,6 +105,7 @@ void TestRunnerMobile::connectDevice() { _detectedDeviceLabel->setText(line2.remove(DEVICE)); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); } } } @@ -143,25 +145,22 @@ void TestRunnerMobile::downloadComplete() { _statusLabel->setText("Downloading installer"); nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); - - // `downloadComplete` will run again after download has completed - } else { - // Download of Installer has completed -//// appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" + -//// QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + -//// _testStartDateTime.date().toString("ddd, MMM d, yyyy")); - - _statusLabel->setText("Installing"); - - // Kill any existing processes that would interfere with installation -//// killProcesses(); - -//// runInstaller(); + _statusLabel->setText("Installer download complete"); + _installAPKPushbutton->setEnabled(true); } } -void TestRunnerMobile::pullFolder() { - QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + " >" + _workingFolder + "/pullOutput.txt"; +void TestRunnerMobile::installAPK() { + _statusLabel->setText("Installing"); + QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; system(command.toStdString().c_str()); + _statusLabel->setText("Installation complete"); +} + +void TestRunnerMobile::pullFolder() { + _statusLabel->setText("Pulling folder"); + QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; + system(command.toStdString().c_str()); + _statusLabel->setText("Pull complete"); } diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 65999439b9..4cf31f6bd4 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -27,6 +27,7 @@ public: QLabel* detectedDeviceLabel, QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, QCheckBox* runLatest, QLineEdit* url, QLabel* statusLabel, @@ -37,8 +38,12 @@ public: void setWorkingFolderAndEnableControls(); void connectDevice(); - void downloadAPK(); + void downloadComplete(); + void downloadAPK(); + + void installAPK(); + void pullFolder(); private: @@ -47,6 +52,7 @@ private: QLabel* _detectedDeviceLabel; QLineEdit* _folderLineEdit; QPushButton* _downloadAPKPushbutton; + QPushButton* _installAPKPushbutton; #ifdef Q_OS_WIN const QString _adbExe{ "adb.exe" }; diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 480d0f87f5..8d69317369 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -709,6 +709,22 @@ ####### + + + false + + + + 10 + 250 + 160 + 30 + + + + Install APK + + From a2bae0b329eca1f0005858c851a33602959de2a5 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 18:15:26 -0800 Subject: [PATCH 012/130] Fixing merge issues. --- CMakeLists.txt | 2 +- cmake/macros/FixupNitpick.cmake | 36 + tools/nitpick/icon/nitpick.icns | Bin 0 -> 448158 bytes tools/nitpick/icon/nitpick.ico | Bin 0 -> 52562 bytes tools/nitpick/src/BusyWindow.cpp | 14 + tools/nitpick/src/BusyWindow.h | 22 + tools/nitpick/src/MismatchWindow.cpp | 101 ++ tools/nitpick/src/MismatchWindow.h | 42 + tools/nitpick/src/Nitpick.cpp | 372 ++++++ tools/nitpick/src/Nitpick.h | 134 ++ .../src/TestRailResultsSelectorWindow.cpp | 106 ++ .../src/TestRailResultsSelectorWindow.h | 50 + .../nitpick/src/TestRailRunSelectorWindow.cpp | 101 ++ tools/nitpick/src/TestRailRunSelectorWindow.h | 50 + .../src/TestRailTestCasesSelectorWindow.cpp | 106 ++ .../src/TestRailTestCasesSelectorWindow.h | 50 + tools/nitpick/ui/BusyWindow.ui | 75 ++ tools/nitpick/ui/MismatchWindow.ui | 199 +++ tools/nitpick/ui/Nitpick.ui | 1079 +++++++++++++++++ .../ui/TestRailResultsSelectorWindow.ui | 280 +++++ tools/nitpick/ui/TestRailRunSelectorWindow.ui | 283 +++++ .../ui/TestRailTestCasesSelectorWindow.ui | 280 +++++ 22 files changed, 3381 insertions(+), 1 deletion(-) create mode 100644 cmake/macros/FixupNitpick.cmake create mode 100644 tools/nitpick/icon/nitpick.icns create mode 100644 tools/nitpick/icon/nitpick.ico create mode 100644 tools/nitpick/src/BusyWindow.cpp create mode 100644 tools/nitpick/src/BusyWindow.h create mode 100644 tools/nitpick/src/MismatchWindow.cpp create mode 100644 tools/nitpick/src/MismatchWindow.h create mode 100644 tools/nitpick/src/Nitpick.cpp create mode 100644 tools/nitpick/src/Nitpick.h create mode 100644 tools/nitpick/src/TestRailResultsSelectorWindow.cpp create mode 100644 tools/nitpick/src/TestRailResultsSelectorWindow.h create mode 100644 tools/nitpick/src/TestRailRunSelectorWindow.cpp create mode 100644 tools/nitpick/src/TestRailRunSelectorWindow.h create mode 100644 tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp create mode 100644 tools/nitpick/src/TestRailTestCasesSelectorWindow.h create mode 100644 tools/nitpick/ui/BusyWindow.ui create mode 100644 tools/nitpick/ui/MismatchWindow.ui create mode 100644 tools/nitpick/ui/Nitpick.ui create mode 100644 tools/nitpick/ui/TestRailResultsSelectorWindow.ui create mode 100644 tools/nitpick/ui/TestRailRunSelectorWindow.ui create mode 100644 tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index d0a2e57dd5..d3ac2bff50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) else () - set(PLATFORM_QT_COMPONENTS WebEngine) + set(PLATFORM_QT_COMPONENTS WebEngine Xml) endif () if (USE_GLES AND (NOT ANDROID)) diff --git a/cmake/macros/FixupNitpick.cmake b/cmake/macros/FixupNitpick.cmake new file mode 100644 index 0000000000..8477b17823 --- /dev/null +++ b/cmake/macros/FixupNitpick.cmake @@ -0,0 +1,36 @@ +# +# FixupNitpick.cmake +# cmake/macros +# +# Copyright 2019 High Fidelity, Inc. +# Created by Nissim Hadar on January 14th, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(fixup_nitpick) + if (APPLE) + string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${NITPICK_BUNDLE_NAME}) + string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${NITPICK_INSTALL_DIR}) + set(_NITPICK_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") + + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) + + if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ + It is required to produce a relocatable nitpick application.\ + Check that the environment variable QT_DIR points to your Qt installation.\ + ") + endif () + + install(CODE " + execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ + \${CMAKE_INSTALL_PREFIX}/${_NITPICK_INSTALL_PATH}/\ + -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ + )" + COMPONENT ${CLIENT_COMPONENT} + ) + + endif () +endmacro() diff --git a/tools/nitpick/icon/nitpick.icns b/tools/nitpick/icon/nitpick.icns new file mode 100644 index 0000000000000000000000000000000000000000..332539af2abcdf60183b495ba04c65a09ba09d2e GIT binary patch literal 448158 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%0DZpy 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%0DWB4CW4mR=WOe;xqhiVT3Jx@ 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 z ziO!$iS1{bj=za*xGGK2J?-m;N;)7c?j+NK@8=}-0bTqX-d@Wtp-O8%C0Wg|&j>bH$ zjZ|X9gT-kEatV2&JK@{J$8Uqnc#)^)yoK)c){cs-TyDTR6_qVn`M?V z%Q>q55UJctKpx4S$4n+8@YurpDB^dzwPZhCo>5}|nM=y`wAE!#=FLd0Sy-%(hoIsf{M2($#}6FSY1{K(gIPWrGw+QR<2V0?b%=Hv{t_oms@(|F z0I+pgKmXNM)63J-GZcC9a}0*9t!?A;=g(Ns)3`H@D9n>P#Me#@} z(n!V63$OfCYBq^--fC`Pg%Q9RL@OIU>McuD7XGMhB8`xk*r>_8H`Z~UpJlT3RIa_R z7>+s|#WO5uF4ES#ZEpJ_Rh~*O3VJ=@^y%@=vIE$KM#HkXz1s>izS^9gdV7025B`o` zM*5dZMkp@6&*h!&(R6m6$L(IotP6R;c=Sz}JCRnguEP9pYMufbJoqI|dKC9|XU^`hh|;}Rr3!$Yr6me}hlZ`u0+~vOp&*5)>alGiGRHah zN46z7#SX@D3Q&A?$Q)3+`%~B2B!aX>B3mCr0_Sv|{FA?cTgD9xtN8 zMi_CzCZEj*D)f{WgdU!=q_XLIy^xbbb}z7L)HA9tEIJ%!R!MIls7GP-?3HnBQ;@u~ zya{3@mkNQsLB^C$wmsnE{>_;+2IvB@DZ0*e#S9Wt=rwTg(bYtPJAVhMHPEAF*%!Bl;5vXt48M zP=a(_HJB5W&d~DR@26IKq7S>sa@cauodgt?w;sWC0k!D_CKM<@yBMFh2matK_sl~s zeoA0H)^o)}=xhoE~s?dS^Gry2uBQk3ZnTbn~`!(;q6%-{ik~(%Jz1gm+UcuWxEpD}QmELUtBB3mP{NP{8GTe5aS0e6Z z+xuFPb3WCHB+zn76MMhUu0U>IuhGpCB;0h0WHPL#syaN^u(a%s)u+*uScB29evE+i zOJHaXI|@n;8D$QhZqvBbZ1L+`i*092x&1~Ir; zgBV;ulxQa>fNhA$^ECK;l@(AV#-9U{f$hD5%mFn!e34l6?^n_gl-UQWJe=fekcB!& zf72iB7u7r8!z?0{luXBqsyf4^hlaUOaGmd^_BN~s`CN(4BCI*X`$u1M{{~q@2mugU zxj(T|tIr2^VCehtA5w?NRnh zN=ggU75eQ_guTKD5^;K$d#$&W8clAeCYKut`h8d4-E&VF&>vVV9yR!NcH{ZC2C1WQ zT_jp&URmm{Oq)KgMW&To-S)2WR)x%2O~3P_$;n9_cX#&%CFf7ik?ig5L(I(0(Q%V} ze6wC)@B8X=_Yu%n+fEc)7~CY%J9yA?F*7rhm)t|%K9@S7v=QHvco{QsZyfSYgwOci z(1cX>@Jg{IE01fYTg!j}-iz_zhN*L}n_u(DIeH(Ip>>f^;iGUENuYmQ$%OoKI?#Y3 z4Aoca=mvK^ht=o^H`HH?_W8SLpc?!*U=9Tyi&gG|5{-=O1LzfyDe|bwDstW)W(RW? zQaLUR(1(d2IT*G2omn=B!Cg}i*16jdG_VRFt`J#>e9w0AiW38SoF@>1ny-t>!Es`Xpc9q?$wUj#Ffqvc@2*?1vN z^wi7q0B3}Vuh!)_O&Pvvh}s%eu@tsq?`$-ZE0%I$#8+eHU8MI>Vi>I0VK_#w_bj|g z03V0};Ki@rs4<@&(C&?;vR9VyBkI7R5?+k5hsu8<+)O6iMBIKtgzw+v+_<#3_(0M_ zH1FLfo;}^jX31lH@_;y!LEu7Hww9X5@a>6f>DSdf#5*@D=(_?l9u0* z^E=EszSyGYV?mMAGsVlVSY>?f_^~I3q5954n)6tJf|7YmtxGp3eQ$4X!pzLfsIH|@ zT;1iSErfIXvyQ3G*oyqNQ&2dHl-WK1cBOM(L$^T(0978vHj>N-n6G_4x+ zpH!i~efD>E66x~y9=YtNwRa7hTapi8QKvshzq|i2fo-$Z7oQ$4aUsFW4Sw>ayT=}Nyx{RxV*qg=^7L~ zlY)LG1%EG~g<=Zbt{T?a&r~XN3d|Cd3~fVLn3=s|Xe5j%nB0X~#Xn2TGQNDig$_PK zr4H>A=RW2zefJTrgJ?KCJ@w`qPxAmhjv!37MQn&`tmz15TBR53Clw!++FDMt}@ z0aqVnH$u4=jD6d+h7rP9@V={L<9o6-To%J)BG+yXfP^2NS-f1bfrv-4DfafYDK<9A;@8EqA z+9)5O^&SANikEBbpMWcX=l%$nr zim~j4dGVay2a7feA#l_+rqy+Zc-tKbTnAOv;q>p{zu)+j-qy-I`QU>8^md_1AqhF{ zS6CwkZdAg=pyvv6=Doudbi@X$uIwfM19kKaCd;PZ!>~Ea^-(Jxm;Pqw2hkTRN6W-b z$=>}0NP$D#Rmbo_6lc(V(j_u~{zGaDo|a> zURZ}C0UZy!5@UZP7z=dzLPce<&Uz$QBHN~Y+A3R!N=_O0^`lR{Di5F~dK2Atccx11 z!zTo`3p^7sJT^8Kmj~#jL+-h1KT6OsnNa25j^l}u_@Ka;elxp2S{smAxFA(lBGRXUc2Tls~9`h+A{Yt`Ik>3dd?q z!LE#NRiyxD?-eX8ELyS?6IWYm>6lR%l}t@dE38LyUvMNc;vG8$rviLP)YOabX~Y9w zKX=3?z(AC!}{3jeyu zd+D?wu1;xwcC_z)(=CY7<#NawF8Zve>qhEM@XfuFD!@_*wmtVLo}4I1nyQ5Os8&dv ziHWJcy81}J#0)n7PF(9%iE2)itqy>g=rGw91?uf0N@uy$ZAw>SE znbky@EBS~`kr&za&lD|}p$X@XehIJdHn{~a`e*Lds2ix+#eJc4xP1SSETK@+;M`B> z>6AyWwwAu>5-6rXd%Ja`3-T`;NJ%3!WMpI%%wNLFD6Q&74Z7(aE{|5CjO8NI@bdvs z5}@H%n}nL)jC}zi5}_-M3MK_rRaL~%k&A*QUqy%F`3sH~%;=MPUS#HlUmL|Gu$j0; z44I~l`5(mGe}+1wp?GM7Ajd)mcv-Y+uxk!C=!3N6H?h}7$OKmG;bI?%fMm;dA*&~I z&HiUQQ_FUK$kH^~$nk=K*BC$OV82|D3crq5kee`PNg>ve&Ja^2WsDk1hk$@5kI*xEQzzyd4 zUrsvqMYUsvfW#q>u=0`K92;I=)j2La#U}=Ead9~kNkms4QG*BpM~_nNr^`R@O+t)? zuIaa2SdvUyOvOTW(<%8ZHxHd5D0zq$6>#L&uLJVfZ{iM{saK1bI9c|eKkfSKzTU<{ zSGRA?WpRCemfz!%uV?F=4Mixgtj;MHgbswjy)W6g@=vHM*2iA{C}S&72c)Q{f(Ek$ zMl^!Ls#d&)4SOOKo#twuP!cR5iz;oZDJ-L@E_ST*8(c_y|7a+G&4b87It=)g6jMw# ze;TL!t@F!_A)w&YfV=xG`QJZ43r?kValK%@yzN|x$HSk0#8tP`d;-&G2pH!6*ln4j zDlvLR6M6-3au>#lx{J!=vmU1GC*1xJ9Si~(0cyT}T8YiLbg?>sx+>_6GTiiTyIhULyqhQ7a={e@wk zqZy}zZ@ChxXJE8_%0N%Q8ZI7qoxSQoNqFO`9lCtSrt4aAM#v`mMewYh&nVuOjnI77 z(DiiR<*V()Q>v2t5n0yNz5FTxk6VilvxeO%+_S<4V@}3$d*3>_w1JW>5;}WbJo?cbO8^3b6bjsK4IJ&Czr5Syt zCG=Z$x=p4H#uP=3du{AuEx#WoJIb55A$9yXc#+f)zPqf<@BBm5baObz{|Wy@vTaIA z%FVZT6yDv0JX~b-k~{#cg5$+%la4odw;IKM8>fSG*M-g`o5s3%$+@|eny{xK2mc`{ey6*@mZPix?}B(GfJ zH-TsJbe^}OXrl4guV3anJ3I3fD<)}G7xLFxA2v5OaJtzLKjiP9B1;eU*OMEs1j}&V zcWmZw*?jtJw^$Wb_A&6!>|SEWSK%P$+~)goK_u}IuH@X1=Jihh3nL^~4g7Li=KpYO zs3a6mUHhBFy|Q@ASBo$H*jRaonHRtpT12J%M~76*3W0rtf~C(Kt=rh)n9^5=*;BaR zJY8-h-Zll^oEv^|T55VCYIFXHX}DD&c57=Zr%3*t+b`~ecN*&{E0+~dGTrT5=OOAE z8XEq$6y<`~r0>S>>|+g@&z>tO*}bzzEO&l-{@H7yRp^>pTpNQ#t>kAtqMlsVql*(A!&-@I>7F#L`WJF}Gg&7-4BFMoWj5ydxN zpJRXcH4q86$tWo)wLRnDsIrk=PJ|FDET`S$Uz=!9iFk=NxvWU{6VAo3!0?S7vx1`+ z*wZgL8Vd{QCz*_|mP-wrz2eqr6jZvHs>zXTvjFtV4*rr&v7sBC8@usz%CweQFDbd~f; zjwvX_Zg&U1L9!h3GYb=3aguQVM*mjK|1Jz~@#`t^2~djoKkXA@1^AoF{gqaC6dBpK zw@H3+uU>V98MB8WS^mh7cRJRz|4985iIUG|G+!B;EMO8Qd|6o&7tYPX?>d|h-}a4pQ?o=wx9MSgnBoKWip67ecXz9FJYLKREC?BB zoVh>idCiufYiL-Z66(@oJWYwk1Q-QRYF8R$KqC>9t(VW(+1a_dxQ>NFhww4G5WjjZ zj(#lWWx1rxCTTsd{x!+B*+sP(TUP>n&-Wi^(F7b`s}`r%FvoI2J_#W$vfhPzC5LsY);m^56-%Zw!J(wjGcSnE)mytSWipVx&bX!*c;s zfV926y&L=p$`Gqh-+!f6hCyE3NHb^2ehWoG?JBD`Lea_Qmz`AM{a0T>@v4EK)`iC~ z9g-ZZ?V$^FM-$7CsO9}|D}Ru7FnSE2xsy+wIL2BcCf%< z3uu^PymxvtI+^i5=>vqSMt(lpe^MgpCH?i(rRB4gNK0#_x}V4r87Vb&5FH9oo>^26 zby>@M#yFhcQip$$icSti|JD)-8K6SjYSQrrZSZ<|9$fa7wXd%)@^a13Ko5zm>zsz^ zTMVK~ip=Uh?dmNWW*F{aGxhM^vY{1EAOFAg!!*z=;M%^%t8M^BFz^6jE5HZz{{s+U&MzQ@=-aft163JCCYR9R6`k*DJ+pYlSTBTdIA#Q3rSdEJda z)j72=^V!v_Y=kYO6(`*L@L6q+1?7K^Sr$BRh6mq$&X8~aGJA4^#TQU{@AbsN>*ySM z8I&8!lTxA-SonfeM_YU2ZAIS;$(4jK5P+Iiug*cYWr6_Vxjk+PQoTzP7on8LU8zf3 zj^NY(nX5`nK?bB*h9C(csDo}MRsK~{vt+^iWBBQ$F6pOIP;)Ilm$5xdmuiE9jI4vo zp3%|KiEvyR@eZoEJ;zIGnzJSE<(>hj5`jJU(@ZZlz-kzw2OJiB-fyhdQd3v*;e#c7 zvQTGCe0!o_{CLIT(&fG?Q?t(@qL&cZfUz9$d)`@LgPA$*CHH)6+@I>$jzv8ife`ay z$et9+a?I!cw+)hLif<(eH?D;Qs@Pa(g8)(e%oJ>sEdn;B%vgVB7guA*$%Hl6Z|uo= zwNlb-O`<9Pd%?$BRb2e^k+;Hc zxKwb)H$_u74>70GSA#v^2 zyiVJkjR>OR`N{dV{NaMY-}s0_dn^~;5b4KD<>LeLr$`+x^oy}bn{7=}^g7u)#N8rol(2kdkDCJ%6hi3*2 z(ZhQC^`XfX&EcyNEhT2==2Xowy2op6m#fjQ_4DLtnALy)?1uECs;T3(UaHo2vPMQf z-bqPGI81>tC{wNSB%g7U37cZ(pqwNw&YBz$fZSrb%DBoxG2wck z{+mT*W+7;_FRiNN|q31$VT=-xkC}7;(_TF+d(R zRBOn#(}W3eeMipv)x_DEIO5FAJKvm^nmlVZ=8jvn)4z*394t1vS+_YnjUfb>K)XWm zJJK&lK4&m+>5*n9CZ2GF%I*NZpS(amg6-yUS{qds4qVavSo(Ys(5U)MSLR>ZUjl$n z&^Uj+DTt@yI4qrc@FLQ*9ex4*>hQ=iQ&P@2ZXX@YIxgGW+b0nrhKa2Q;*?$jpulGP znaVz&0>N|ojzFHO&QFLXGR5~S2NEx_X2j1>Ud{9iJz?b)BsFSZ!=T+XZc57*pou=_bBtJdpwDB)Tda_J|OcS&wkISh0{l%Q3H)jojF6fe+!wz11y@o~6 z??8Ze-=8hqYiVl>du1vv)H%8zqq=2{h0!A!x?Wdx#UM~)Q@@+U4u{h&eqEhfc-lpF z_aSy=Yws!OzhH!y2KWRm-vK}U5z&tTI^u&*kC9ten)#}Wx-*^wxya!AmkB&Goh9E( zN=@|UlgGWhspYhdEw;3&O#amT1f@$^eiT)O~4@;0J|TFuy- zE{0}NU;Ed0_J1@1#XG_W389Z2k)Z&S4M7&7Q7mOE%!_3CHFc`;zeMK0K2*PBbfTuG6W)3``zX;ciH;#XeEiQ zFm`zg77G3R#d;*wJ4cxS{{sL33h(2hY~DBa^YhcTuvpJ|(c7sW`;(mfsa<3+b^r5!Bu(T;?31aIT zmb~eDvPE|U>oI@-^SKCEH!G22m-Dr?HT|_o1QYeF!_w{X zt=v%`Zu`PK3d2vegA7ze+fQz^RFA0KgeBSkL*O82U3}wMeOzov5Lg?V8ux;JlupLg z(<4QC<_)2tp-g0CWU-4^SP}u@!`VWb#|X+mL4E*KQ*zWB!)>oEURPIF^Ju{JO}P#v zsDs_-=-u|q%o(;awie%+DLiJ|L~16r%&r(=0}bS`CzgxtAwRY99eQL@{~Rqe5U;`C zgxX+=WkDQ6Kx#r4vKLZ6O~hXiL0R5^1F3gfdVi0$b)aI3N2_~`v~>La2Qhtfu>AST z>BVD%o5KcxqLvns$M(1-o+>cW>w4rkN~vKb0};}{V+ZJadS&srJ{*^(0k~TD{>6gH zY0{IX9z4M)eP*@=fABOFYxui}*be^N%$GjplsLXFr%4Q5v<& z{_Y0yIpFG~N3}-(v)0@_|zd^>}zMA@C=mVkM()B7Ew+Nm{ymAN6-2>(`*7f2m`KW z_eYsR64Q4^2s8rTNwNjCA{;O1(sin7f@WRwe{7QkjaXOeUI@ENfWCJ?Vc!P{1=&Q1 zgeEt$6(;&IPqW!raX4o|Mn-)jQsAd>wcP zjFX{3byhvK2wT#-1hf6Fe`-f6S+_h91`TrAw>(uY^v%D~4K*T`bwQCQ)_@ zzlTwN_`nQC+dg=v=BW2 zSHS{NvYIc~rf2b&V9QuXQAVGWnfzy&TI51e_L~6g2f$ zDVV+hMj03RZO0!c84K*ajel+3JM_7{vQqVP)2}Ln!9vKYrcG?=>;voLxhZeo>dZ`c zvW>Kd3`4?%Bptf>&Z2&Ozp5X*s`*kHwT3vpmL|RZapBS>FfevCqju)-B>RG+dR`eF zk-P|k7&hSewcm)f)Rh95UC5dLwWWfkB>R@R?-r%-m`o+lb7_7R00?G<_t3h!;kf2z z9#09)UVvNnDGJnPF|2`t+;lBPAb@_I1A0|OMXgFI?pRp)Xn-rc331%8XBy98p)a+e zNfU(5QtwdzhS_LprIaVU!_!>#yj10pJlb2lOIvIFm}?ihMSkZe4jkgc)mvgghbX|) z6Di4;Uig;_BUhX*^U{|Nznt(fi+G6s8h!y`l82LEpI>|TXFP;~eDbvGsoFCz?B$AJ zJFIY#ZCI#J2nsH>=nrQ8n+XZ&cFv#&M?m}YlChs4GvMLf-d7OdK=4P!F`zSACxzCC?W^W0Zy4kPL!z)2*S{jbWp3!)pWyyK`MXq{9Ju=ws zI*o2Z3V;+Ts>`wby=&P}4@Y3o5S_s49tT;FCV+H(`Aya;5&pEsJx^aLvb<=|NrH}E$jK_Mxbp7-CBY%8^1OjYNlodBy( zFkLe@Zmz=O-!5RB_f%>IW&Pf8G0_LIHx`5(eKr}-9c!MUUE5Lza zH8DHFkBqX+ZP0zeQvB|BXPy?0CMs6Lh~><<%)O(3Jm=O>C=9>6)(rmm@nhfRShb9M z@7E;A`&#_8=l(-#ll^sOOZY&*;4MwaSkA|dY`|F0BBfC9PEvOEtv989e+D(ph%tDX z&5+`mO(3_^v!_$O*8R1k;!nmHPo6)cQxDC^8p%1vYzad@Yv_7cRB^JM0ipt{dt`la zq{+VS8>#-O_^Fin-#BVOtmJDKycQ8FfCr39-$MADsw$L8q9U#DF;5-X+RBPKC}crq zoq~y1ghmjF-tPKzJID}(uh>F~4|vAJbWEC=d~Eiu8)Ds`d0FLxp!3;6Sb3jy>y0j-BFiHP@|A|2VFVNHK-Osa$R z6hb?DH?iJ?x-_P;4R>0))0b*u(16On+}sT z5@u(xDM5YVq&+<>Kb^e?l4m@;7~ckAiX|!COBg z(aw4dllapf&*aU_YzkFJ)cqKH`5f)7c<9=6!K%ZtV#y#VUL24gaA%zpba=~K&hbGe z0!LugBb9g8e%=GyIE#-a)nRu2G?w_lRj=KN^JgI!@pSyFFFYWY3!30x;&;e6E82dy z1>WoX)L``!kO7}H88Yya;053BR1b)SCSt;mlbUY=C?PoN2D(V~x7EY9MPqsWS2pGO z`T3_0^IC9*<5c3^WL>~ZR6ueS!Y3u?^4fWuWjMOu($cc_;brOhXnHmVySsD4qf>=1 zY(;SbgxjP1L`JP^7~AI)XDIxSi+-Xxhz zq18pI~# zcu3IOZtXt|kbxU0Ljb!t{k6((&j&Sfe%H5UbVs+JTJxPL8tbfcu5YYn-!xiLdW5bx znDykjECydqBw7gQ%>!UHmIKMSoEilpF*HlJhnznRX@*Imeer7_U681)%s^Ehfg7t zC;+fC7A9@f<%Mh0jbRg&d9I!Xw+dS4t74FkW1RaHRF@)z|m~VtM$NcOolAw^S|R#SjTU zg&zUu41vZhU|`TvAX=_|Q&?}vzZp+)ITLk1dQrZb61Co=`XlR9$B-d~>@S3p$1L(O zq+Damf~bC?bJjpbGJxc!@y^G0TMEJYUb1rX@;~V8zKHB!skMai(%s@#Jg}>*catGSWTcpcLQF^AFBt(h-NUyK)}BHRE@onc`@pG z8hMwJuZ&e({N5`Y9o~Rj#ytXxXMaf9UYnFP_VMiO?A_6VWOiG*hf3${m#?_*Gh}HS z3m?)J-w{4MD!P%T21&T0IXQ#N;q8RVA_%AJ2F&qtk z%pGMA8s2q7dg5{wZ06(m&MMU~Kl!+;VaxdO#rDLeumqOnCpwE65?&z!`TbA0}uf(SP2+@nF>vt1%Uy~bc49sNRh^Hc!fe#Edn+A1=kLU^UqE#Os zab1q|JLVqK{nG?hboq~w1H1OfKa(;VmaaCT}ZRn{z110)Z^xR>&cwK>-NC3WhbXL>aBjB@UCurqyRCjKJRBB zqVzM!j@s^^{^2Ms%zdB^Cca3W@bAkAx_AN_#-_X#bow zAAWs)5RV>bz}HcoLyi`lN`G{|1A4~W`qOyL1_uk2tTofgla34h&kxXZ*2$1B&R2y% zP|sFu+V{mk&Q%NBUHIis$gL^zCaR3vE2qc%FXrLrJL6Yr<6uCY!`!3$%{<7r>wE6% zX|(6hpF`x{z3+coqH@)KvwSGy`QlwI%&D0FcueEyo0yKw>yM(E%bDoNp1<%xy{;uO zBqYvuxgHrI0F<`@4QSy8@bztc#c}cy4B-3) zdG^%y?Zu3oNto;CF>YrlrMJEmiSthNCLG`Ax{ITgi95f>N!*(^d%DVB(lfu~r@UmJ z6Jq{Ef;@(I|2rlp*OCrg3sdn8e*AlDhYyUA&6$N5q3DWHALID9^@pH@^;$tNp&Yt5W^v|+fm#ERLL)QIcds))>=^}>0C zj1QhWeu=drANG%lu!#@KY3`hx!J?R-$g%GiC=elQL~NId+vtK4br<{T$QOp(({T*r z>Z-Evn>TmWNrpGh^9dyRmQ7W#GU9kJ1MTw>8)h`PA~*$^Ye&*$9C-!eb`hWgh8 z!3q1C@Ys8*icbbSU;W@SFVtT?=^fWn+gMZh9@`axOLG{hR_t}!sJBvSKRyVm4g>&r zcvd}0vuP*fHu_SltE<&-uOZQl;?1=q`ZXvU>_(0PwPDxaj^)yi5svkrntz_5k%l>=ElM=-U@0c8 z5#3AbJ`rtu@5-OMjEA)-d3=0aFDoyv^h<6LE3siJ;LWOc=1Q$_+&&T@9l4!v7*O3x z^`cSzXczG~`zU@aSzLQltL<9i^14@BXX0y++L8;tMg4RBA+?xLNq4+wpUT6pkWnUZ zUWg35NqBKV*?ocwCZ}OL(Z1VB-I6Tm-~PKHEU@SsQG_qj;*2 zxL*IuyE6)@rww%&!~iEb_L2$5%CG=x-4?&8*#ia+PX|$qMj*}2yM8+(t+2DV|Bw-D zx*@8dprBydy)Vzx;R<;M>F|~g^GYy0q2VFfWsH{|t`b28Qk>AqN(sy}gT&gy3g)FjC zF(;881{)8cnlJ8@sA`3c3n#jr&~iFwb<9AfWhY`q7e0MnTx=^|4m^#y#{ms4v>BTS zCj1-m34?{IIF}B4v`z*Evj?^B^8)ytc0ZtRANa9y6z5hGOLFGph9J+Co=PlwI6W+HSz>5Fw02d+eMv5_~ZC@`Du`Ad2_BE|{O1HP~zr zNUQRV@zMAqEp$7`PvHl<6jaV1CmN68@0}ND9^P9F>~H}t8p+qyj)qADiYY7XJpGjb zRl!k`=>4VUi!;^gDNBY85Zc{Q7p~)e+}ycNcmjz+#a(X|g6ZL?Z21+GfY z=B^K`sdM1hpBWq-k(iu0z{H2*-=DF%v}H^a$W092Mp9!VPuzk1&=kqH-)V^zwOfH$ zmsk|;@wqBxe;0BvYP3_8$dDV9jX4=zT)pABAVAQp?{}m$${`I6jWla(>yq!L?>-Z0 zX=%Z)Bfg9}?nlgX1FCJt=%1{F7A2}$SXwT{@JCP>r=HvUy3LcQ8|Ya{^zf1Q%E@&` z*Q=t$ys^{!sDyTj6I?4duhQj*I$u_#ObLFy7GFMO)Jm;L`Wq|7CE;~JjhxR6qhxoF zV1+N#_hUk#D~8QpchR`)5zx0DLRa(c<=mIUG?DtC`Dv0`O8`I(T2aKlhip-t9joyA z_3V|E&wl3@rv=qZlYxL7z-h4Z71vx^2&781&yj@whBfjmD_D^OhTcL zNrBJ(yqs>kU_sqHzN}Icd)cW!qT>Tt~%5WaD@uFqOERyWIvd1V0YOR~+ZL=0o_H^$AGI&?#(lROLv3y!ei9wcisbQ;;3%K@ zA8aTWcshLj;6ZPh9SjTd@~3Ofi=jn}^m9=WIk~Rfs3#`eqNhcd$D3a%qlE>#&10ws z;Jj6O=-)}2;Q4G%8pRWuB6Q%}Sr?s&cRj(tvG$^H@9lB9WBmZX21@^Z<2)QY+Lp@7 z%37*=@K-H>$x3I~F`7qphfsX%;%|5yB2T^dy@Blx$!6PSjCk}6 z42-_+#L($JogHG4H`YU$M9y3}qNV0#0Kh@B&4L9c^++hR;CQ#4Hv>_>KZola#AR&q zsa4UDYh`)4N^r(VDXH!yow|l$S-ry6A!g*8wx?I8Vq@O?3py}H+Q@LcA)BLf6=+dP zs{bcE8L3ylH9Tb~EPatTefCquKAVyI5ot@iY25xkr>W!@E8ttoKuqBq>0ifdU-xe{ z7r+4RDkG|QK!9zw=?YOdp5^T|;+g7QRV9%id(4v8Gx3WwWjcQDPMp%j*hG5umzGDh zdRyw4k$x7x&xIQdv`|!H7UcQESg><6QCBx_rc{vVX9pI_veM6YS4S4mW?jI;VBTJ3 zWhD*-ad+WZ`*fBc2<`7zT9DI;VJGDAm}|R7)H(4C>etN2Y^QECKCtBnZxZEQ#eN}5 zU_m>_yMh1bPRpA%tfnEFo3MNB2s+?ejM_sBP_WbD+%+B8GmSs}DXH zddbDhJE^HFcTGZo082{%Y_gN(w2@0MTm+5_Ed z?$PK85c+c@QiMzC+@-q~-M zy^V{DgRQyow0OaVUI$9XC?DhZU)9hZ?Jcp%VW8=y>bC zDEsGq_}pcYU2sx|fiU5|M7{5=0Q`T|!V15Rh(>kZwd`mG18DE@=ez_u{&) z@8|yOegre;%)I9~CeBf7u}kFPr4zU>^>;A-siCRGyU}tq{B>VALqwj*)sa zFIq*@pyNL4mb%bx$M(-l|BuK9B6T8g@Eu&)=0be*&Hu55Xw#;=BCjfjTEg)uvsAp^Dhi>8u9M{vi-xEb#(no8YlGSuWDA zaC;0xv9tIzrw7?JRp%q)D|!}>{64T$)+{f-*h$OlP)0|Fy#1Y-w{wSEAchYvC(NW( zQf?LqTw3&M9SaMxL8Fr4L`6MB3daBSqo|Mz?@T8LxI+Ah4Uzfxzn50k63$K6{(RrC z(;l(eTO9HJec4&;r~4RFLqi7`b~LpsjtDR(3qj0i50-Rd_1DvQe&P%mIVU_gatgm^ z{Z`d9V^#9{7xhykJ!(54L%|yp>NMO*`%a!{*;E+FjA=kXC)r7NA41T6>&VMJX%aB> z7#?-};Q@uz*GU8tD{RQ!%JRQf%spr@q$+S-=`=V)_mp^E`sWMU=OYt;{U(PgNhv5s zUJhi)+IlU;AX*v|OawuxwC z8ErLjx}Vr&jGy@{BC%*?nJcj6FBlDSj^PjF@{Zd|HD5fx_3V7Z4_V+}`sv+D@gZ$c z8*YxQ<49nR)A$#QprCEUL5-|elxdSdgonGs_xpS$Q^c)0Lh8FVT>dkIMMnpV&7qS| zHmNuqPgqam2mx-xN{&a|+&%7X%40ulyLm)i{C7V%S{9#eqonC}&aQY$T^~1+C>=Cx zbZao)VfW}fxkd_tnFhd+rDk~xE zw(&Fee?Sr(j&#z8N_x4~0FHy@iMwJ;*@-0W;)TO6T=EN1_Xhy-cBI&1Pt zqelQLgT_C?g5FnelFOD8Dfj2kAMd4Sc=kg*G8Z>J0Mllw#h2Zi8|e$f#?k)=PY(Xs`P!ai*hB+@fW$$3D%C<-X0yns+zP8j^*WU+~Up&xKA`W)Z0W3fc*>#vGs=F9*GI>MLaBrF1CnK)yc zw@Ye(wZwR`k8ShE1fBWfc8P7zUpx(uGX26gbq1;hgd`=e3W@2(C4YKsk6l-5H@?T` z>I5pv%VVujO34$W>pO7-Kt?N%#?AWck$OSo&o{fgkF}mzB_&^XKk8`T!M>5c@tO@0 zq@3tuAIPlJ+=|1Eys6Z!GTq<*?!5Qpx4&GL=?C>7VBPa29x2nanZT3vU=EPHd4K_% zua+y&IPnIc+UL7-wNWDDe$)74e(Ft%ROz+8nD5y6UJ4~GX#E9zyFP_tBZjVkF_a@1 z6$G!fMJka?C-82xw6rw%t9gu7ZQYj=EZ@~Wmr=6YVBW6*8NT*0;ix}-3JXgN z2Jp34?ax2sF=JG3F-w9=7PZEZ^B2Dj9@XNoLBqE@X4>=Qxm?D3D2LwonFPtN6)(q( zcBQ7@*B^zOUH(R79hFpWlt_`Pbgaup(zkOitO`jijPS)Zm-fg1rrq{zKHM@SX~;{2 zN0FmBjG2!%N_ne=UtY%f)hgu8j0wMe#!w^O1SQYtmgq%;08j787AuqmVJF#RRa_u~ zL24_Z1S@*IN>|U3tt=(=cip2Z~92$k2Vu zL*P#fj?SOYmu(_aifF!Waz^iA{}M5@mV|}!*8udvs9|`*!~^+``xNePb8DxQ%U~ zjtq%~B#GOHZhvy8U~$#PYFysIVVCXa$f(RKDXlPvgH$#|Vdo0zn(8Fen78dJ^p6AI zD?$?#7q)jF^SigbsP$O%o(Su!S#LgmJf)^Z^yiCJAYUIB?uGgiUCgTJY{1c=V*C6`RW3R24m;~s-G?Rf9YQc$ z!TB&(O*4u{m7iVqbEAwF6r@7Bk#v_}{CK1%yyA0_MglHM<5lQlbEpLKk?_>Lz;1v- zX8cj8H5dpBTg_2A-{M}ZqrSY{=W)VGzUE+wJ-9txhljQ}%Sm?HE5%E7nFw!CQk7YIf?t3Tyiby&M#xE{G85`gZ2dm4P>)GR`2>6Ie zRo?whm{4+hoDw^5vZ&Ykr2FXa@@XVs}OfMxSVA`oS7nLp%{206^mN# zF7TYt@zeGWefKV6W3+ptwKWzx&A-^QpP(2pP9|=|44|eLTP3XNmA$<$t>Z+Gc%Cn5 z)<10cvnj$8OIEx0OIG$El8YUp0lJq6KLCNTqCfDm&7UC`7xD1qC5v6nnF=B{RuQOj zqu-Wcr1Zf^ZTPwUnT6=dQR{((&B@u@gvJ@%aBy@@5P=t*zcw|>btC5$97B) z;oxf71w-RIQRIx)w0>0|{Oi3)IDRQ5M(*tFbUvD1FE1MGuTtX$R3U(aqa%+H&u@?N z`fF-(Q-#ow5S7m-ZhK3krH$tCHB5h!>pv=ypovZ+iYwoe9Rp$U%{c29mTT#0cJ32YJAg6xVVmdhajs{28@ zzjz9y=bx?#1Ao#au2Mz;dpyJ>BzsOqs^7P7g4bV7e(q6HP-qd$&(A;oG$gpVvhWh1 z09+SCILDhX}^?U`vWAWt8EmNf}3MiW3^@Rm=KI$^18~< z*Wxt-!abVA?=(Qe5%tCFmojqS9*27yT8{hIuFmh`Kz_1q_`pNpc}fe1Y(^Gce6+HY zyKS=;;|NR&J>Fu9Sizzzn=|#s-qo|@2GYCpOhk8br#-vJNklrO2mleqk9*TK`=UN( z)@zmRkr>kb`lEQozjp3c|Z z!2$@OUk~F`+4?~bR*fL0k`*#mtv}xj;a}4*X_e$fsVh?)3w7r1Pi7&IPIl1xH@X2( zm;+n%?aw*#$RAIzA0n^>Tm)(XzBP_;b1yAeIM$nzLltKO0kL|#aA;xT?3nb3U}3Z> zq{)($Nqr5 z1T$xqg{|pB7%rU!uFRw>T6$O59LEuucP#CTGhzi9wcp#y)>8=MY0-`OhprZBe(`=9 z)3xocG|_~%#4%qz83gU3l21N9(w{WP>3drmJiooRJ8)*sXH?CNpZenO-ryi4%&_LA zpBDK9Pj}smVc~PXS*q@>b(ONed=hxfzv*6edIbhfu_lv!EfXi zT!O^W(d3v2B?EPy`j87{D`|d}Itu^j+}FVpjKPgJm)oFzFZh6&oPj~2bu`mkaSI-M zO%;{*?ca!PYZTt;TsB_j`!(IHS80YGY0}tHa;T8VVb-B3^FrhdEmHfl407#?$|E01 z|7TD2XE(vBKW%^e$^T4PVHeJ2IoM?UyO6GmwF{PY^}|He>i2MBxd zQN7)Jkn{2kXj(Hp_%=8d7ScfU2u{I?xE%U6N&2p5!28Hj1}X0p_GG`>5&BB3>#0x5 z5+2^r80ueU{Am)qd%@-On?^f}?(^r*&(s_*_ScX2X=*xa((jwEkc|0zO}fSj#8F@1 zXxwYIfl9vimZ{Be3MD-Mon9T4S7X|7pB4WyX=V7-{WYIBZ7Q~=Ee-OE;$S#S(E|i- zcisJ~bEGT(&GquJG)SPyf6;YNHJ_x?2hR?B$p8jSQ-1iDezr6OMz~hgJ$7e}*q9EIz1k!;P1rPy{ zakL+1&tOvLP7_T1I&K48O76V>i{)VQ_xx`5qulMFm~RA}Zl_uyaMw`OA3?vXkvZ3X zI7wnu$Udc9mcJ-da#Y^_wDAW7)b&-3+FQ>nB$R-7V!7zY*wZF}DZ}ZcrXhQ#cO!6H zA4vu)wS*%lPY?KHUIw#NN@Z-slJ%}ZXJ#a5$yjz02^`K2@y~sy{gcx{mM4Q|T?~?M z4-l3Sn3~%T@_deiuBj=*QyEc&i}E=oP5-MXmK-cQE30x@Rco?YU+BH$Vkz+-p44}z znw!V_b8g(;r^8E`z$AqG#C+aU#F`ru(D&XDwen4Id^#`sNK*8s&pJl({4DW$qq~Vz zS7DrqKX(D>nJCv&8YhQP*PycZ@v2i`r6q-A4Ck|7NMx-j2caMzW;iVs*z;uHTys@x zJKptuM5j?CkF(QoPH(I3Q{(N0pIdW7M6W?@8mZz!2>;p_^2j@BG6DYFcK|1`)~^rP z8TghIB!7KP8fyPg&(_{;J|!Y!s3oK=E+u+ltnJ&P02KwHJEdNmU*fVL*u;NN7%xtT zMA>mKD}3zq4`&I;D(IAZt_-i2dWpF5(-$#3av>9Vf%7!yM)Sg2p8QQ5+_)PY4KSZFu zQN!(sqr?l=yIh!`Z$DaORD<#(BvA%1_>&~bo^?rM1QiAm%?wQH+tyNhk!#n4)|6ky zw0nXTm){+Sn9@H|cO@fWTA+hy^gRh-ERiHqw92_h>pWC82!o%JlX;jt>J{VmRp!yoi?98Y_mQfd0$<-A8o_2_Ky64S=rNReOTtW-ro``<-W!r~Jh9UYhJxrZu6oA3ZC zZhd&*HWnx893v>sW7V<}KyJjryfEv?|6oYMj0alF(5nT+IAI#7Bb_z$w&bD8kx0%y z;M{2l`quq9U(W|8VGaAC;oJP@ouV%O;z?lB&pRIPF5*807k7eu-ff+*f8c?0a6AYa zAKzQ>5c1kh)EsF4sOK<70zMFT;o%L!PM0RLKl=oa0??{;R!9wBOpT=rRkX~WG?+Gm z&PCTu!u>-;gzg(n$J0f(y~(CtsgZ1G|99bSCh5AfpVB(L(HmEsUQQXV%IMF*hwD}{vQ#`b2Y;2 zCfskR#ke0K0|6*AoK=}0C8j+Y&BLYoytdk7zhb{Cop;4^*RiRp4)_LFHF*c^==Wi9?c%-KmiPJa! zuxIxmI`_G$MFW9>h^OCmnFDkVOo4eHzxbN#>=&(Gs`0#1EIK%rI9C(n`8&oJ#(X$; z@80d`CJ!ILx;@jhKB}H$>LJ!k##(=Az#kqT+`7Dxii|idt4QC>#ReSyDo8mYi9n-N zAvt4u!+dj1hP}|*D5_74bI4d;1Bl^oOqKiJll7r;6-&(;X=w&g zHrtR~XXX-pA>F%WqSr^y5yGBM^nOOErlpJI@x$szIp$xRhJz0{alrGll|k>G;a=oR zM~-!eTPsrNM0$~)yP*=wH5)74gLOVq&((Q}$XNc)BL~Byv{TH z_o^r|jPb6d%^a%bJ*4A6!AXFfz5RpOO>F!n|L`K-+splqci&l#9M(h)kLto>qKv!cimL-AdPzDel8Y`cO3qWh7D*)L8-e*IJXDkdP4U zgXa7wm4vv>8n4K1v;(reX^?t)dK(phoH~#vbeJi^AI?N3LI1d^xTuIy<{FC`v^<{9 z6@G{2I}krOmT}GIC`H1!^&EHCgzn94wif4W{#a`*ShbFzx zIJ-aKL%9G(BcL-?&WtC7Ib~^5W-35I+oLlQmM3Jh$wJws4a>b+aq0=*WWiu&sB#)^ zY17ZhJ+HG2XkVhJ*qfb+jYlZ8oo^F)y@2J-0Dk-S02#17R>XB`afWR%mwDyz#L;p4 ztx)Rnm1`ZJXVuKB`DMX`K1P)vui1*C%T8r@jGx{?AH%YtWQsfkLvQNE(|_C^Bh5Ns zf?CNf$T+3nv{Nz2ks+$N#6106}FsAA&nE0mH-bwWxogb__`GJzlVU!Sf82PFeDf4h*-P>ZR<-G{! zkAydF&Krz(v(u#yAZMSC9!Xw%Hhx^lAc441n4hEWIJ?H9h)vBT-%1MLy-tBZePvK| zGhgdEe5x&JkTy6_U0(7iNrHvdX8PkRG+{u=kC_MTeq3oX0XIZ@KNC|!+1S_~SJ?I! z_ZG(<7s{&w&vkTGR}+Gsi#{Scj`>nB^}s*l=CqS~!_LRH{JuC3w(D=P>y(Tr9(WGk zA&Xihq7`gW0iQMcAf5KJ&vY+o}K4;mlZ!E(^}gWSG6$?yJQ-RyEN z;1^cF-Tpl9l^X{Pr9FI4uGfk6H?jDDCbwCW-tcp(?*_z0Gq%xIp7?&30f&>e12je2 zteNjh5Qt=eR<%s7BDL@ym!LBf2u%o;q=K1Nz*dT!5?}R%)!wg7gA8cii!As=@d}M{ zfpm?wRhSt|%3D?Ve?0{qoG2qt+lE}+Og^+1Ez;;L(D4BU3kONYLNHTRj*h>R zZWt9a(hFshlah*6P7^6EX!mz>KDpGhw9bmkGzI5kUr8R^fygmo?g$qSxQFUkhUD%vwDL>i`@N-eYOi}lh} zrY89O!szDZkeK}t>*1l!ciC_8pc((ayO~nlBy6C8@p0<&%6GnZE{)m0M{pJA<<%zr zRg2zXKa=6@oXsmyU9TScF0igJzl;k-A;TaOn0J&(8V27J-$kKNQfNJrDs7jgw(7+D zpu*jRU%g?qy0s~giG_V{%VI7B7XmC~&32q0GOSR29wBn(Y83dvNi}QOfd(Vw5OnhN z#aK&Svv0}N-M95AJ=m1ykztyVFFJ}r1g9}V9A29Ye$&qbjP9JtMsz!LjFFdmEH$cJkBO-woxb) zJ0yv-d(-mBdXL@XZ=6)xao}Y5+M3ONJSZ=8t<`hh&-2sd+57`#!p;mI{+k;vf z)n&vhiAhDyi9RUOheD$95W8*K40)+X41KPWOgQN0kjC2VFUNzZp<+BwhD<*bzoD7x zKYFc(dLv_FaYhdabhoy)9xbgI3$n*TkS{RlAK$H1s0m$T<%FcZ@uo{#8J(%RG^!d^ z?{`%v4D7NCf67cAe=Ebq;a;2vhrIpIRlN?}qiXWXNQ4+VmHvTEjRD>tHTtpYo&_5P zHKIN?(3|X~P9g9kI9$&BgE;ZQ+9Y|%Tc0WQ29OCo*gN-i{6(jZl_1h8F>IV*t zzy%ETcfYCWg{$k+xi?8p@vPLkdU_wzZ?G|V44QMuP??M4s`#e{_W8{WzlGojpHS8< zzrUBskw<+?vWLQ;36MLZFyQacXRJVFq6fld%)+Ic@TFqQ$ij0nbI)8{&0^9hGztzj z)2#qlO=TwQB9Hea8B5yQ6b}z|zx!)-16_?LMJ_toL*1uGDSg1xpTZyfIbY)F6{+O& z@bQ^he%=1Ap`=H0ecDNF?7^3BHtLW%ro<;9v$=i_K#&MM2sgIYd{p0}nC|fp1k$_f z684fus{M`%@R;pH%KQ1lR$Q<&x#-vY2;KxqU1RbSGL(`hBk6k0r{=B7s*m^865w6; zyl4a5zI}bsozmVfrVeoMXR29t=-^GnN?5jqtHzr42?z-{T77@r&zP3m=Br;g-tf&`pDPK$- z!wDe1?5>lQHc69Dd5RNBb|uw9rPPN;$HzZL{(MHn;o)uiCE~00ruC6m9~t0~s@PXS zJt-&&HIlkEa~?x@6JJ)F{M}eB+P{Nl8F|EO;(fn2MUZzyT#E)uAqhHl@z=FOb81U` z%*q6Vzmk>G`AkB7j8%v8g@-w6NJm0<=Fpq$24Q%=f`HAYqlk4 z{&${gv~lC(q6=aAci0uB3hB87)tCVE0!%+bY8#n$I4~7=7()6`XvNtQGgY1E!6>b%=UlpZC97k1iu}A5DdoOdtHyEv;0X5qV;CZS%AfnnU4sm8z>^n}|H+XX zv0kO8p;MfLHCqg1OuXdJ%XxhBr_A<|P=E65m3@reNtTya0V8D)0A(dB4cFBd(oN6@ z%>^ObAP!w4jQczMmLWD$W9WK&&Oh!z4$b?lKhY^y7YX`Ab3w>C*hrq$I3r&|w5QCF zhk5rP@05Eji-`c~B@q!()-oS$hXajYQADn@yTD*)r^`VoqkpZpogD|ewe3)c@kHK2 z(=&B>Qa^^(Wxhs@(1@yau%<@$RnJNMN~JFaF&GgQ2+)d@$yFe9KT#%CN1>2+AYC6c zuZdcT1$6DkmORK1nLoIr-&(Md0kDCCFubC|7-fKNo%xEhr24UiCx^O*=#U-HH*u-Yx&GRvMai*4S1n(tG-K)F!O{sAXW=CeY zLR+whGD9yFV^9^VON`{zc@Mlf@^>O;PF&dfMj$?{O z;iBz5blj~SKZfKOaW?s`11GJ{fm?9_iQZpr67`CVe>`iqFg5!jNrYgBq2v;7%uG!D z5?8c5D;r`MyewjB@B3~H={(npkdbWhprboQ0pz6oSrZ705k*@|35Q?jaL5wud0gC6 zZEFiw(2`MFpTycbD}~R0eH@B5))_ZZXkKVI{Cp)*yY;i^n20jTjwpAZ3vKAAJ>ckk z%(|V2wEpoQ*q2t8$>pvbD2zQgh8$*~2HoqJ$u@^(OVpnU2>%&eSH=}&|lIe!*UCDu80^AM&NFXalK_eWhf%#tt|yj2``s@S?|@N zUEU0}Zam3rO#^BPhuuU&%JceXINM@#E`%#Vb** zv(9S8VY;+QnPc*X0Qp#}%g;ZXk1P!>5d1WPSH zegY;djui%&XzDJ<2@wzY?F9prKJIqGFXU$ThH`=mER=DrB!gM#*S@|9f` zJ()MH4xE&69+Y@A#6id@NP==ODck$fo+*7JZ6Y$bay%yb)mK))-C}3LGQ?(-My9uh zJNq5kxcMXl22>fyYLruKqPnVz#XbLI8IOPN{$u82i`P)$6Tn*lt>1}WdU3g7 zU4X+!jT;C+A+o6V3U{@lq22DNDxpyX2<;Bt&<3J;al$BCBBcH)pes<(Kmof@+Dj2r zG!jj!FYQmr;}k7Iu%?&R2fi!va^~?5hCmC*<<}}E>TUq64xCG0n|VgedAL<-xLpgp zPsSFR_*1Fd-CGtnJjVm$s-a-fcS)lGTuB8wyUSMXWNn|%y|wBl;w2WTlIAIv3dU@* zy^w@&!rCbYX019P8Yl%JIx?NfMF8_tO8vI>F`*d2bbDr`7t%L0G@xb7Nw`6!FK6ps zRDm+;j~+%3+N=!cYNltE^H(H9DulfoWu!T~lAXK6MjCVa$7qr+<7fl|s+dO@^B|wG zILdMzc_>(xOkZM_8I-}*;~r*+_eZUu(3xiaE~z#S?q^7?lLNV)pcUfjQ^=f%PgZR& zFmJcbd_7-NkezK+EuQM^Ccf(?G@vbAJ%DaPMqptwX2Iv+U>U;}aMy9IXVK)2o zo}9h7Q*TywJHjG-lmc^!?~s1;jAqCChJE@?zR#4qC;SdiM~aeUjhiJ%_0GqVNt zBnfRZ7Yk(LUK-O@I=Q-*ZuCj)6CN}9n8ScC=zR6HT0S?SYwU|o=g+l&v48mt-}ho! zNyN|hNI=~HjtUS^H&YWwK}|XMcU_~>kxF9DAk!et1L2YsUB$CZ!1BVOLll;gdsC8v zT765W-GLv8VeCz2Egtk{1iH=0v!-;7jDEf&IDIU{98&S!58XD2W&>j9(IuM7znDiN zx3{*wkCLLg<%l3Oay51mLj%G5!zXVd_-+wIE7( zM=U0EL~L$snK${NBYlSePg%7!ot1(b5e*wix-gpERP$&BDSHR30yXCM;#Mz;sbYL5 z4O?gp+Ep=R)hmp!HOsfL$GZ%k5xTb}>yz>74FZPxxmQ=9P^6ycN~9Zg(~~A;q?BbX z8%JYf(BD}1WlCJUx$x0;U0h$P?CYEk2^XR5cV7XxYa+@T_3Ms7ArF^(BH&A|X8vnJ zyw3H5Y5PmDJ$)fvyJuy@E9%+mqH>ABJmR?wAX9zZlhLvUJS1HT*vPdn|6yQo)$A znij|3u1_K9>F&vE24WH`1V1M_vL~JOrEZT)jZY$w(blFo_-ZN-XLlr&o}M1v>jq^7 zWxQdJ6~b$K=N2B3^hT>NC*Jrj3)#uD{TnQ%g75vGALaGoFZCPLV*6dGAXjV{1@iDP z7n!deVNsBUP1$>r=&E6(jh+p$xgB#sF6;l(N&GWpVECq~{H^#py%pl=03@e?4OlPu z21FO1iF?}}X&DFvI-l!4e_p6{m~`W@x=XoY?}KIqMk<3bkH~mWr&7dTuTfJ{zL)yD z)+#cW8Ke~XGfX&NQ?EokdFwk@jDG|P5`g@^kAXZ44?FvyWu>4DJ~&1Y#i4Ep4qf_E zskJ#I?pjLAoJ8wbhJ+V=OS4ZrW5wXvXl6_jnPL;$wL%FYg-~C2D>G6q9oyKwLsu%4 z`1@j&RArHQnvNf@SWC@X4A_BVZwT(}wz9f9mza#m^<9m8vRrJFOYaBQg$ybI4JbwA!D(me>bcdZe&%M;Nv!{{pjkJFQ}x59BbSW;u`C3P z$bcCOSkmdl->DXqpvXJh6Qz41WuccP3_kW-Kj`w<9jhw|+7M@_$PpaG(qj4>;b4Wh~0M`8{POJ?nY(V;-gPsA9o0lhGPAyum)JAT+ zM+I&8n$+}OY}~Me)yKp3&xpoulIN{A(Wz*TH<#DR&L)++`ZdGSHcQ4H z9w%FBJFJs=wGvSVfB`!&avboA7PwDv`~$@QW@qAw$JPAgj4hc}Nn=CM8%9ayU!!@E z<7TDdBbCt`KJ@?y?)nKJ-{EH-c2s;%$^z!1B&*c$zessk zM`wPT;Bfgmh-96@3h{ITGFKwp8T;nCuXz|4@prb3q!sDgHrR9+YoLXp&}fjYEs#yV z!T$5r=w(0yxrtYKze)T&_E>S;MDXh8g`65zBm)V6=iugnjEXDjuLt3|u&dQSVuH>3 z^Do5^xBZ7luDiO%d|1y81VWEvh)6tIGIuty{T)-+0hhrxhT=6s2A)V_Fk*oabbr7? zq5fw$Gs}`Q6E1oI`aHktXHAib+-1t&^KLg}xkDj;``rJU zW1O)qs3VZ9m!6pV;c1*>H=hrK1a`&hAMS`Huli}ZYENw>~k_zbUp74^@P zapzD$1v|vl3Tru^pEXz6YUMv-oEm-N zH+`ZY*QJ*C%qQ+M;-jl>8R>-XIyzq_KqG^*^0dNDUkdFtx?E~8XMPeXEyTGDY&Qv1|k!K;wI?U2B#|hf)cSJ^Y_qRxuEMRZG@z#$1Kft(8;>J~IVX z!z~(#FeCtRg3fSrg9cxR?1{l(oZrDx=98DieNtr@q=Ob2tYCOpv^lS0!y*@bz`zK)W^IzzCEb zCabe=pZ`sszsfn0e_PxWP7a)$oah*r_UkZf<%~u4JXfVlV!JH%^H-R-#Nrmkfw0AJU3zZStOJK0!9oJ=3v%^U{Ut8kCee1mN)+1t7LU<;*9|U zt;7=WFgDlbp2ISi)PN##pwaK>JMhDLvfNrAH8edWNSwI;cgiCTfRK>TO6QfZMySQ~ z%uGFZxVM{Dd+8<1!Wpeu_v1k^uA`}=xV1tj3PLaf4hL;Fk=@3wk9AQ;vOGQ*1RqdA z`M8*>tXF8Z^0eoDOIj@UK)C4VysBJ)IXrc|!5$@jt9IHz(OsAL@LHy+ zi9<^Qs!v`@>~Kp7O!z2d5msPJ&aRF4ah_a~-qfCvluh&M%I;TW=KhvEQg4p%#r1ZL zBM2Z!geX|GU1-3{A{QjNvS<$o2?=!vZR?yp=@KUH=lG|zdFBC;P4p2BJW@teO1NgL zW7CDaB;R03UT$Qu!l4NOc_}}0V2L=DfpFXJ0|-!!=b_K86rwQO&1;k^L6UGI0)g0icNwlnE7O# z$MzFvcd^tK7Xgj-sH&P8pX=j*wVp)nk;?#sk8fXiZ&L?i(7#yR+jb`kQv1B1FAny- z=UX3;-96S8mm-7YKGzb&u9wG~TyC8_^VVl)XSV+-gcT^}pD%UV(%V*SDaHnDec^E! z%UETk6y|p;6!4Q1NENemh;WoWL;&DlaC83}e&$Br|NMrcg>;QTKj#8#y!$6$d{ou`L#apSeTXs=c?bQ%qcW$`0EpZ_bmZXZZ@hl+t9h(sl7({Lkrc!Lt>$*)JRL|h9A2Du1qh~cDh1u;RH^6?nz~?8U zgy>l6f6AZdA6PM2YvPxEYy|epO1i`BeF;xiTjy?m*p#Co5IW{k?`JGJ@m-USmM(gX z&JuIG@LxM74<0;76c4z$>UdtXL*QNr5nBFvZZv$aX4{&3ZB!}SG&5exjF`{ou3zBvDfkb9*1SrIpNI?lysogs1k zBol{y_`PlSSwWntA2){yatANZO72}?Gk?VFkdfMSMxg*GL;;z8uiWNC8a?Z};r`+y z)MSP2N3%N_X~*PeW|4(;;zF}rL0r2KR1!n_p?eVF6~S!w_wP@3jYI)l~bjvUz!#ev3rE$HxHE7*ZX$bWtiOTs90=c$9ZMCty2z& zAV|yy6tO;8j+6j%BZuPPqnpL3m`vsEZ~_Akz)u$hZh|1-G~ zw7HMEU}8l8;0sp%;<^;7nvwP4kjkw+y1QA_BvrZA%XQ^RKctn~JueOfV%RS?FY`~u zIK~gX>fYG10Sp0GhaZ?dz8Srz@T&r4L?{oxSb8#TI$4)0UgvH6iv1g->UwW>*-6!d zlMDcih_VI8y@E>YPEHADeoCBTqX?=&L#i8C=uhTGTOiDC!Z>RUnZXh)`{IyqR*2prXncH{S|}!hv7HzuDjbC;~VIE#w7r(F)k8 znIF6ti&LZD;Tl^ z+nB#jR0D?1XmpR-3MqH}-ra|;1OXDU2AP}mw{uA&Wh4MeVR4O{O3~)S6`hb;NO4J# z=VRQMMQaAI zcx<0w`w5-^f+8b>fn%MdXNTkvBoZG~04XKbCF50$&3Ls{RK@H<`~X@jhobK)95_z+ zZ)^;JHAn|8_CMwbimeXY^=&N7*Z2WlU0qacjujs@k7F*9)Y4RZ({#Z|os`piK~{Zr z_tV8*uUS3~qfkA9{2Ef}&8}kOcD((nFro ze+ovB9<)?Gkur(l*2a|~5C{_U08FhJ7Xu6|q)i6QJn)|s{|a7N3n8G^SPQ&%>VEUf zw6{-FLb^r+nDH1lS>4J5Ir>qfvX`6B50@WHUynQc?o}0S3JUGlQS*wi>`P-nZ~#pR z)9(cjSPT5ZXIX0a|CLMwrq=xO1$cXkGJ^q9@Das*64ZWkrGuXq2F~6G@Fw{svkohLD8VI|{374x-anrMMym$% z;Jo#oe@I=5yB0LH;U_CEFK?JTkE`ni#r^h^G{Xaom<^P-b2QhpG?~6orq;-ycmG8V3Gza0 zJw-rdK*J-i6pcn#GK?6p0m3h5^mB5O;zdXLkS6v{P876Xq`B;HFy%wf#Ls%f(5LkH zI3PGQ9*71O|0p^$kaY;YwB6Ou3>_F?g-5AplvgI`YTRYXA?>FVZF64bY76Ka%*n~Q zNSVP)6gMfdnyvE~6QC<31C%1@R2*$(?GdAw0cAgj8cMB`@6N3EB+Kehnv$?R^aCl$ zB51+NU@Li&YJc($K}1cZ<*0t?5IkB8%JT2(7XJ2+;^ zr=uey`iyvXUx1DkUs+lulR+&Q@R{be+cVtQ>|AWFw%Oh#aBwWc#>#<0!NJO4=q5*P zA|99uBv$YnUmeTy&vYDs{82Fb`b|WWuNNR{<+${i4w%?8W@Q3sDFu@nVZ{QhY|E6Qa4F` zft(4b0+|;)0kW%J$=PY-=G~4~a|T@3mxrQX7zzi(Nf7@chXZIN6aj-n6)BeJLEX?) z9m|mWOsYb|a56Z6_Ju!HiWn4IUZD%QUiuZ`Yu;s$$~jkH+UR?6oT3x89vB)LTD0s@ z06~d^Fm)KonpC1c9t-I^(e+F%sxDnB0C$oQ{Wdm+NUlQ`DGFi*1$!c9`bqzjlav1M z1_%JYK)5z$SOZ2=b$Pa~qKpY{y6eF(a)rLjYEX1v^mQx@b+SDk|NQ~^gA*q@f@h&V zNiv2R>4ke;T~9vK$`KNg4Cgpc#|U+cU2v*lJ&QFL;Lr)u(vSRLP4I*m9uM3#1vPu! z>{8=wzby)?#RKb)zHt7(TL1{xB+QnI+bQU-_^Uu+`a?%5)D(y2N~24z($hbBoS>&p zK4i!)j4AG@m)|Ah^Ne)D_z`lTdUs06M9Qx-X*I=nRIU)~S*(34l*hCW>hGMoNoaVr3V6d#5;r7j=4(Qmx= za7av=MYwTxsJe7b0#A3#mAk3TuLH^-LdBqA2ox(S>}6mKG2Gnpjjo+|_kTM80B9Zd z{7c-xGnKyp-Kpap)D+~Y)r_q~sgAMZ$%3>sIHRDT`#Zv@y^?`?C)biOc5mj#YxX=M zrZ$L;R{#!i*!Cj+~+SW9^dGQs3 zr&TPe8g4x!4^Du$8adU))A?+~Y{KlI@FZYkXNNnI=+eZ+z3wLe6GGuWAl&p*U^Zt> zy{WADogsTfx6yUpT?i5p3Mp3suMF@|5P>h)Fp!P#u=5J6+IU5okZRB^T7$sr&?MZ0 zDXk_lfb)i23}@!eghqY#-P`ufjD4>)MAZ%~c36UxSK-`P#0JZ)_HYf!z=yb%A6@>4Jq)_Ni z3`-|vf<1)?fj&XdqTpy7pniN%_5Ts|-O+GG-P?D@#SFqAq6HIO^b);{=)FdX8l4bC zi#~`F(Ip~!5MA`%Ceev5dW(oC(Q634HF@9P`~5j<&AR)XbN4xWpZz?~zH~Tp?$>HT zd8-|1ENUt0Zx$lP0xHVN)Tuk-(*SKf<^jc0)SO=BaVO zB;P}HQzRDLq~bwpQ-OCHCSN;llxDF2`8^^1cIT@fOo{l{zy2K5cdwIvq^) zk0Q-%71Ujpdsa_sj%6YtMAde1kU#y7j3{prWh2`>KS!!8zHptqQimkI7!=?Cq;>`54h|}F{A&B;)M-NJ#)W`jtFt=5ZX9ena7Sgqx9luGr z1PFdJH2i3bV~YSFO7MS1Y0>|Dp#yHTMB|Nsa)14#>JS3JUMix-RRCAN6riZIl#m;O zpk3%~AO40?^CClyO927WQ0+_r@i-EHw#s#Hj={!VjQi<#^~5NT0Wdra?En=g=3n2g zd06va9G=O1xhH zs+xeM@Ad1~wmgs-7JE46eJTX`-2ferOpu1qWl&La3e~~}A?mCN=W3Y4`pRVzOfxF@ zeXcEXg*rsAXDVrl5cTDxehzf&JM=ew=s$_`q;zwjO7E1Z!pwCUgF*EdzsT}>s{s_5 zktPolS*nq;a&d4}ZMjXSu{e^T!v*CTy~_u`lfW8P(6MqxA2vIbf+bU9!$6P>X0&ui z8OrR|X@X!!Kq*+dg&K5vKHFSvF{97K{n|FEDM)&x5OLtyD;Zo7QAf!UR)B_vrZw|g z#%pV`iYy-Zy}4azh94J273O!;0oSmWKPO<51waAjf8^jY;NParLRjuX6uU%HbZq3E ztI@m&T?7Dufa`=RfWsTt`ad-!=xF?>zrMZ~6eimzq`L3eR3zM{MKe+ZNAK?L=Hoej z`<-+65>Eb@&Xe<_n(N+Nq1{MzFg(BZmdTUdT9qe;uP(4jsVw^HZ5M&Yp5mWC@5aJ+ z&G>j4w8|(H2wA7Mln>COQc!%!3}}()A`+A>vvNR;a2*k*a4~6Z6UU_wLGi$C$TOH#pc|MQTCFzC z*Ec)#7p0LR1+1V~CL%c962DuTe0^b!-?Mb)CT zq(r_aoP)ODw!UQcafq}84$77&iDpKBqdG2)_+E9M&#ugdz?A560ua?1B7!|vtKphS z%n+f*rO)>+zQ3M#_~R3-l-=@+dM;q!=*hl3r_?>n0#!_uD_34$)C&`3SpdvOr0Q5` z*metOLSIyLR)8p9-=>C^lv2IDq@E*ZC@PaKbrLl&^yaG;uG|{*eRDU$- zf%^ZI?2yP1R58Kzub4!4GECwU#A)x$KA!%>(#XctLGaTz)<~5fzQT}|BO>4}kZOrD z^dNJ`|FmVDY+H*?6R>PT!Jvn5BSkuFR+E^o40XV{im|ymz4DwG3D$bL_abW7*Mser z$;|t=vQX#~vVX4Z-}T?RCIEqh%-lHpo9w7!_YXs<^z`(a&H2m9{|W{y1Q9X5?84>o zV>-t4-)dJ6_!&ugCBDVYD9uYKoJVioFgC+ALs1bRS01rJEQQ^g(i-8`+d!pFy^f$X zgC#$PtMi`?#ai5y7+mP?e+*xa{Lk<-90^Itrq|tG>n(k?rzn}h;2K)~D3ji>?1$ec z{k+d+W@ajdgl@RXNyT~eX>U8YVDIJ+mdwTU%;W4Vr>Hk@k80%_M@BHM49}^@eu3t^u3`vV`TIura*_|fQz{s4F8$s4r z>6YKWjJ_m6uJAyFF_z(XTq0ti`` z{_hU{hhidE?uSfW+&456{5!Wh>$5HIMIC!AKRQ1CrRPKun$z5zHgx+OBTILJJUlp# z3w*8#r1CpvkjUY>01!kJ3kHv>jBo0GKiu|?ck>&fWMHng)D_)+^f$d$KlASX+e?wr zsm`6pF@)eLZG2<%nb2_KCyg}8g#wo{rD>E zaxRg+0QK~o24f8N$fASLrFQYOGB_eR zh0=$yf-{3n3Tg^kzsGX#sC+iT(CTl8kxNU0hru7`2-Z1&@#GBJYYC^5+sB9?6L29? z8@ePWAOCqg`BR0ZyV%pa5cbu%s6bB2n_okkZh7LH(p4v0(L%D+k4I3Qfrlm7;bDLEM24A>BZ?%xbf5jWEfd(DSd2cb$2nU9PU?|d3#QBj1wn}bR{h#t4 z!Yu+rbng<_SQhu6%#vs~y)S+2jfb+eWucv>g#%vd^rcjGY`u7ARTVWSvgKij+0XtY zbVK!HfWfj1|$nOrC_nqH4_aUU}*3E|x_k=%*kO3u?qbWca z*0>AJn^$pLmhS$*U9lzED;$AqzD(&4EXmJ8umFw_)VcLD!B9$-Q&~b)-9(KBbc5j3 z+cQ3cDo2a>yoJr2PD}+;Mn#6`3q>zhKBC}@j$tR%vLwJ_86n0_aP%8OI6y7+fR7J@ z-3=&?>i40}>D06~pZpllva759lxMs6>efu7^rtE|%?$SY+}yFrALomd z-LlUG7oUet87fH%mk7*uz{5Uq_8p$mJtM4c%t-krPAUjIC!h)sLq7#)jKV3>vTFXb zB-X*-*?z+lj9x6SAQle}=W7q^FE{i$d?UNRF?Fh=@OJm)k+0DQ0N3q1R63%4-EQd?zeHp!-NPm-ely+wE9@C-5CF%-5linHF0uOs9Vg|%fM29 zXD^J^QYEmA;&}S4zSEyZ_N;vNVNo1sYNdpSflH2%AQXe`3Hx z0?=Wy)W@7ZM0eU2uQQ-I_;Lyg;f;Z-&iGYM>HOu)Jdm7F8*4nY%lA*)#r3aDbK3V3 zVx)kcSeKmfq{i=S7YXQKBmkZIZ%9ph@L!)HWQ6B%E`P`x{`<3&cWl-5Ca(S9tDX4V zk6v{#axUi22+JNerqyk$|DvYYZJ7L&Q@$}E-v@mW5utGI^uxy1_G6#uSb@vdhj!BC zUIk|T0Upx> zx}R_sufGQA$yPU{1H{C{G$iLg?10-86u#-esJ*0W(+9>N@8UwWz$`Bw=Dh)GnA#5VAWf`5o0;EmK#;0@?<99FdKsL6w1y?T{V^F@nz6Tg| z3|er}M4v^ZODb2IeQmR2moNiJ*o}HA4O3>#!6AK(NW)fliTd2}m(5dZ8;9%Q( zo{022&J4`I$rdbjTw6nz{!%ZnDy_a%f!vQneX$(dv6c_0p;qAjf{mKE5EV5b?)VS6 zUx9#A}pBf6hKTEy4?oHS+SYLC>PfF=ee|x z)MUcpiR=AatNVCU8%jDizfr`6JZ+bEarI(R3u**4!#vY~Mkt_(XlGQs=diMMcWygJh-cgK>+YbFRx zt;9~NCDum{uQ~QVp^juxG$90Dxdvvw3!saAx?9n7wiKWbMJ=ei&xg3LcEOq2wQf^U zL+8JkO+9sVEY`j5Z%nsB(LYO01{~UkgJzI37|iIkOn{@5pkRaX_REN6g!={?0x>C zrk?(uydElN6c$sFav*X-+F=L0NC&jf;z^g4aD5iN-X+rQ|BNUtD=SN$Ny+QAb%u%t zm5tEatKJpcrv^h=G4hNt>_@1V++Ur+|3>KmW=L%f+hm9T@|=d@!XGNLv!fFxHrR}> zvoHl&FYIL!=vFr5h7Y2U*3hB10%Sm>q}snaUV!CYYKN<?1;-UdwLjvCVJ95(YNvx3aH>R1qIew0UM!I7cvgkI{4T@}7C)p^S~+M-j(* zs;(8jp$3d@7&iI;f{g z?mU9D&Kr`kZPd0J? z&hViAITGheC&EQ&eOi1>#OWgV_=LTOQAU^Uz93|jg9U>9Q4JJDU*)0*xS zfHmLf(8C%v(C{p3odOlKV6Vvd#~RkTdwnJ5Q!_==ADPkS;$tGB@-hLNE|1;kua4g@ zODR<_NZOqf$p*nHt;0QVRe(4G8zcaYMp-Mory`af0$|VkMJYGz#RFxcK4Tf1jiNHm z@EmcbN+Vn8(4x*J0p?*~10ir_;d7cg=k)91E^!C|TT4R|XOVxQQ|x*c-P?4>T0+Bx z>mr$??1PBD0O$V7$6Ue1RxXaUvyghI`i2?ize&F9?0XA|7tSbEG->bY>#&J=6D zsRv#43@6@DHfU!* zAo!5Q@@1F$n=(49V^JCM$vZK$a?5Ss1Ln>@(ryS2{?hT9We>j1GLyxXMUnPk8})V7 zs)L!^#Td`~+x-!$ZgA}4>~v!2<^VLiNLJd&DsNF9VeEV>0~Lo`dKsT3yM!3jJ%s-o z<;l;yfN8Py1}!*x%r2(oR92ZL!vZQ4>LD3*w#YkYw`e$e$5XsU z$J>-26$d?=TUN|&{$>35;0k}Ex1v?9&8Z3a^cMk>h9`{1p?ftSZoVLdA`{r47e-PX zO51dMXTKyA*?@ffi~a~_S65fl3_3F$sY{z#OQ3 z3+8^1yMxrs*u;)1?{KEW(V5WOH2x3gm`l8bJPS|>Vs^&BJrt4x(K&2J#wB zjUPYEek}YhtK%nL^GVN<8##@eRjW79}RrQ)sh83Bj~M%DUjO(WI%l~2>(0r`xR&9yyxv5 z%CRB)`tb`b3k{#ZHL_-u%S+0(>f&hus@IpovHH%Av^w$EHdnDhG)JL2HuC&}OiaUrNg_q;Zh_p<_ao;bn6iHslzqI)87MN49;Q+H^29Y6C2@CRR)&#eEA#xi!96 zm%sR?#QRZ6+Ly(w!*Z_w2>x$m*2w^Aq2g7)Xm8XxBeuXdOH4#E*3ofIpLx?6xed(B4Av@k;o?nnS?Oh(1D#*7|@1fK%hX6r7fWU60EFyEUT?iGi!z2{SgQz4Dm|b~UXB%!@=h@%IdhvaTax}u^Q(-)B zW}dKa^!(Yn{%p$0V|;5kkI6iL5MS=g@#Kbk8uO9Mvr5x{ou$mKYL+MDT=#ws)2x(6 zOopOosgcO!VsW}IOA*wT94G*F9X&kPLg4R~ai@8pJtsb-6IPJl1 zeCwGlyifO!mluaa?2!qCP&?D){5y(?q*eW)%q#^=Sz|^@%Z*OBZGS(~ZjKC_D4+C1 zsFn%ZQQY|~DM5(&vTI`-!w}Qwsjfz)p!jCs^pBL%MqY=XpN#=##KJ%RA~M34TzP`o z(i<-caAbTb4!g}B()^sqcZWV^SGJ;^x2yYgJ=1E}D{^=DpyMG72DlW7arw-%T}%zE z))(*nPN~u%FSu!uJp4`z17$HpR*j9x75!5wV?PMhP3Cm zI>tf|&(ps1FhW7V`I*E98chOzMJhffL!CYJRpAeOF{bXfRWm)VCF4d~o;vP4Ery!{ zjt0&ziJ)%&E~MAW55dm2I(f%W>op||My+6}XCz4g5i3?Vpa^?Yymucyjj8<8q7k~> zA8=RDE~YX0M^YV&fwZV7)maRqcymN#WL|~!t^5uHI9mJ_wi6EGyZ6VcPX;gN-N--` zk_q~iQi^HyH5LMB`IO1a$60*M+G;gy)4Zp}V|c?$>zi4PfvQ6tSOcAs3mIJf-WmK| zfn2P)W`8t1ZTKjUiyMa)TS)|0L}l@yNNsML2$y$C>A;qbp`qcgZ2@$nsLXC`e1nNX z_*Yn6g@qzCr&WIqBxHybp97)yNcqC3UI{|1o|LDK=i1?v>C`|!eTXlbh1lkmw{eX! zqt`$BUpJWeOw36zd`!RoK~j6&hEqj#Fc*#Xy+Oh)afwy2+E1sKWnXtsxFFiOpB_>< zEU^KT1^BDNz$8vZX`xovd1>jLrtVN!y&D<0z+#EV|CtT)#q~ZUE1{>UREM5$KDE;zPj}<^F-Q`06cxu z0?VI zqWLF-l>2gXA{m#`SbNaXL2gq&MF5d$}EczbPIcw_R@BNY;eM zot~ey-MyE5$Ts+@Mu6hK-Vm;oW7GM6hO6WC@_*@;N0!7coli)5{VZQ%4mtl?>%Jyn<~-FxM})6&;8tOO7%P?X?K>Z zUkqjsDMwOYRO>QBh|U*r!j1#$m{&69>2pSw&m>*MJQqd}%q^u-G!&R2tqgPyNZ1QG z%->tA&5q9P=AN&qe#x0(==8NmYMF|Q%F1GlTi@$1z9-554Ot=L6UA~PX`%0fYMx4z zR_HwKUZ~~m)a7-~L|xo$}SsTT^QR;eHv^d=Di<6Oq$-jFi=_uUZ^gDS` zNy+^(4S-w(zYousCws&G$2;cvENSXOK-r4GLKXm@tDxZzsYv%i>KI+Kls3N{#CmSu z`ZaN6(HJLP=SjTz6I#r3IwC%3460wCDGsFhGN?&i(~-72Mo%X9$966^FDxWw-`yOa zdG6Iq2{|vWJCF)k#Kf@_XcXk0sH0PL_sKvMN;;(ZaB`XD&}7q`a5C$~uGm|g_*^wW zKnxcM2_p6b6+!_R8vPXf+Q|GW5?iN_EvN83l2j{lG^uA0vHBaTGJ4MSw*h5CG&U zbS%}vq^nKE0EAxI*dQxP?=*dZ(&0mq2|N%%9u-|E7M-+ys<5;G*__!TxF{tY$$j^% zDeL;W+QUIL&`Y=C@Exe^)=wjm*B9lS1k8o`#CeZq%lu{JZrTaDm^v~kZgWL`=I?i< z=8K7P{qR5QA%DrWCBFVzhQCO-17U(-a4#L-ZlqSv;Q;BtdZku$r6Nb9r;ZRzR1tF_ zk)>lJAFz#;yD8;A*dqEh!j-PZ8}QxlV%V$HL3WU8ss@t`ICH;x7{Sk^S}Xns`~P|s zpzYq5&!5aM=4DzHUw`X|Y-hcQB`!J(G9{u?>}vR?FHeSM!{vaXQ9fjSv7W10F7OxS3f3xfz^X3FmrxX*7%5qdXYk6ZqR(CKCp8M3WbGe)X zuF0^uzmUijBT^I;LqPrp1%RlW1UTOq0n;j+AB$t2==lxjvtEo1m2SQtF_&X7k$gR% zrWr(0qXKY(MB0j7l-=wCxxbyc3DvbHT=(<}aZUf!oOqUtORXgOoTV$VAI6EXYdB<_ zCXl|5OIKBfroO?J_5lc@l5HgrYU$>N__aA3kMDrp3n((E9ny*tv$0xTrLh`^zdEaN zay(RO7~@QJ{?mg}J5b^M?EUSbx7APXMsVvz&OY`oa`^7iLNcKgbRuN-qL;~YIwey` z)Y+KcFjDmWtN}Bm%Zu?Y9t43vp}1e|-CnVq;_=G{w10`6+G}u-;J&%-zx=>AC;GlD z09G?u;xU>!k4z`Va$Dr`jvv%LI<9;DZgg-9_W7nbXzR?KG6^8)?LRo``R?8P^O|xa zV^0RP?0?|0MMD)M_wzK}c^5KqM+Io`r(Fs`34;;k!F%?4gwEt}BHwDbiH&Q=Om>xv z!W(L}&JWWMIEE9v`yMqyq1dqg6$PrGE%DI*YI3b*afnGloMWPp4cn_;%l3=jdUHkI zxxQ4&vN^}^S#k6i=_l!v5g{a_H$hHNF31;J#C`-S zvS_V9WTyK8voIz;Ftg;|7>IIoEdDc;G(eLb2Efn&>^2gzz8y+>VEKl1ByLR#c za{p1Q0w4+iyGWyM?OIzDjwO6ITHLskTv#A!(~hs>T>Qd}54=eHz?IsrC?|`|LKDlA zwF^+`v}&x;<@H4|&V^sp7}4Q)4J~jm-DEF%C0zfU71^Px?5TtI1Bdz zpFK53mf8_q;ze$p8ZLdnrfZ+FMZ(*wt#~pYJR@3e%}}j#6@E91M=W7MW2hG-S;`>xtl*W207Qq zXifwt)kY>OFTzqCahO&E8~zKy0$63Wr;m>h)t9@lF9gTQP4s5a1a;YF(;;UV+gmbG zcAE*PQZy7BrBEn*#{UX`u*{%VmnW)}wL+sl6($O_GQo6#&t^?aG<^+(aNdv}2vI92 zzR4`N4Jo7HjA=ePj|)3)Z^_PA+xshZWw*VXxQ06JH0|h{ejaPuK=?BZfW3TZ>2Pe} z`r%wV=Pjrz?cE&odpn@!L>iLdig`a}VVGG-=vjdKnHc**pKGH3wA;wH(*Dz25gLkr zY)`v>wxc_D+`YY7!6W)QO>t2)1u_uzBJIB?5y~KzqNk~56Ga<%v2?5U@HBEm$>|lU z;xP|mAe1gI=7DxAQQ}4d*cbPV zT$#=?^Ei9VT0X!88ziAA!Uic5!BRJ06Xh{7=0U_740_2g@j6pU4|@J@1ZodDDQ(Y| zs(3D3u~o)N($JgQ?;7F-!icYbC{)Rizsga#|9N~NOg{9oD?1J+`t?yB9R2L6(qD=FU|KYVGW_UEk6I{-J*BxKc4(HYMhzNA^$54}6 z7s>y}d^6^y6W5MrG2QNNUWcI)K2P%weFf1g*7oO)+Pk{(73Na9PmMAkw@1d#nT+s8bRblH_(P;=x;8DA73#DoqY!iK-%6 z3s}#vomYMRdZPe$!r_YTQ%;MFC#l(txbz2kY^3B9|K~QJ=ULzMP4#0Nw$4-4J@bkl z@-bgf91Pk=25x?)+#F)sDG1U|(%#L^$xsl(Qt(;bOh?IpQYo#YTa(q-r>CdH{UPnP zf+~t{9Or2iwOpX=&(kkT1_%ls?7o_7QCK%zz=6l3P*4cSPv*z5^`l#mb+ke{fM4pg z@ZH3%qr&mcyfmxal_+7R=z!Zd$vQUj2f~Ry!qLLwC6d_2v-f4L1Z7>YY>%oxQjY_B z^Ny7|f+Di!J=SWUB~-ZGfGXs+A?Rrk4_D(WkE$e@k=sa7BY(bz)gd8A8nEw0g08Qx z*N*dmHPe>f9|+?SVEcJe%EsP^0ziT&)Nokp%Z)Ph=z2-l6c`mtCR`qt=O zZ*3T9l@|&IMIy6M?{cW?omStAOpoO$wX`~_tIJ(nT%1b*`d>N+D^iAsH4_Da0mW<7 z>rF8wi__CrUEAy$jkwT?N$B{jOnZx8_|vgqY!-$l5LV|(Kn zD4W#G$`h;s34i%j4PjFxJ_J$8VoA}}6}X(i^`3Or0N!Q^6EZX$x2Q0duUL96b+yBy z=Adil74TVXkEsBv_=fpm{Mn<9!4EqoN)j6#lE-A3M`4j0^Rx{*mP#JRi!v%Sz4g;w zP&f>YHih~*CcP}3EW8EW-p9cwK2}-D`V_CApx};kANwxx(0HD!U2u28Z)$2bHj{XG zRUmRP9dUuwHUd)}!bvIakc&Jv(KAS!%Dw1J-D`C}rim+P3S+LSPF#h~` z@r$v+&6~oDNG;&J86Q)u_&UANoXLh9^%dCvIZZzYfl?z-Kr%$=Onu;6u^@wV>D#P} z<44CwXBH^VLZQO%%;dg;jO{4JMAD`taMZ=YaB%6`x6|P9;T8TSFTL;kC712BbUV&& zDH}BcKWyN?cv<$9=gbiYV8oVIHTbc`j2)UxNJ66++9ibSSYMHC&~;VkT89doiuXZnIK6>6TSg0C)O|xlg@K{);U^D57 zI%gG(j_LczjD8lmM83jS@vQJ|gN%Ci-$gt>-(cEhNm`EGd$g*fkdRQuQBE6jOv$(d zJ4btPL}X-3w+N#z&Z-?C|I6jgyLY9c@2{pb%-9=&1|4K*#Ha3r*R}kG}qJ0*M zW&)3WBEN~FuIQ30&G?v}=Rd;T>51+iIu?9DyHd`ybL>jHl;0w5fK8w`dCIem?7HSHWSYW6A_p> zp24(-2ArIn6BdSJoN4t3UgW4n-M0XO^5c}gfucnJPs$xLX|_RAR+42Dh95cwUu)dU zNshIYkE{D+x%hrwUu(J954Y|3&(qD~(ZG=?a zk!2Zs+w|;$+~zmX1pz>M;mWf(IbZWwfcxgh-f%R{uDc+R3rBa=LE@Gv9h(Zp^%n|= zf972$zBM@VC@DJwsK~1I95rQg+z?}ZuoOOmM24Zg0P-_4&uW(#WyL(g-oc1y zfr}as%27@yMU0GDRg9g`lt8S#coHdpgesQ3W)RQG=UVUFoe@br@Z2mY;@M{+5QRiQ zb?t!3x3R*UT!`CG?lAJ0*M{f4yXf6gBnR$q^WVM7*N9 zdSwd``;eI8gzUqSaD?>mWf$ic zzIsa6GEF~#tI|2X{j>SAY^SKWIdWkvy`hv^LD5qHo}i6yXu0J4sN3L7Hgv*qQ3hU* z1#6I5825XH&QIXzm=6XzIv+Z4^9+R`qqDPOWl6voiK*Wc+6}fNV=HKmrY;l^1d?t^ z^~s{7A=`zC>Ut9dR$QT?1q4wG_uM`oALQS-O23G?w(X?#9HDJ-aIpEye9TQ7z7G1^DDBTwS}%kPiKT14Sakfl~n1Q8*dq=1vmh!a+enagYwAq@?r&TAEmn`c>(j zF;&!*%Xt+wa?qW83rztm@lf@0w@;BP!XeJdfI<0dHBA#^RKEt)K8{wd!K@+L{CwfT z@#aU5q9h5nx#kvZ0@?8sxD$vNxU&9^ua5#idBg?{mH1mpuVu4rJ3u*x(nU#m97KyV z@J)g!=0>f*j`Ing$INz~@?-KEl;m`nwk4SV`BHb>F`cEMw`FbDXxua6oHI06{jjDVn=?1q zJ~G07Z-8>(%G!Vs8>_gWx>Gk#OQc<(MOKqPsufiqFZD=m#4D!0YQZUZl&>2Hp1zWq z;AW$|xDP=-gQM4xws$aNr^BY6v;!an;g$%5Qc?OVywv)()mSu=As8uK@)5G!@{A>O zR$x7UIpx4(MFp*1TyIPb_YTu94mZ4?^RzhrOpgv%=y@$1 zn=a~ZwGejgQ1L)mnE34Uw17(v|9qV1XH6qDkmepHPOxtjd8su`Qva%p(}k+j0{c-I zVk?2!SW2@VALlj?Y<0`*r=BGbVDg8R-~6svt{bdyEXsan(>HJ=jH$kqL6Y}IWUaa z?iHA#c~?!Q`9RWdcfxRtfj3L#CyRS<7ITS7djnB5mMAlY6;k$0>5kOOlcBAl)UX#1 zS1cQ1Ky=zy1#8Yh$q$;7Q|mi#)*B0Nnmbq9pJ-)>l8O*w-qA}1oL7#5sJ18`(b4sz zENaGPKr<9|4MV~cWKQX)B%eCd1s+A)9v}Yt^zr8TZa_(e@@X#%xujrj^28xG>gP~O z`OKFmACrdK>6P@rv4D%54(T1yT~bEuKrE`S>8>kGVpu3qeJO4 zwK{HJ-G~W|Ei+IF&w0v(e~m`Z)5!?~8aOp+BB-Zr-%Jk-F%z;g_{I@Nb^2 zcY`BX!CCU%e_ARs(HSP|jyk*etn1P-zDC#O_Q~|`O%l#Yeg?X_R+206YFK_7wH}z1 z!umVx`}gm{Pk55wumE9Ua+9oO zntH3_qO7a-^2$)u%RSf|_rJWg#a0J5G{s&uXXdGa z666R>_{zZwY668JhbsBm-o+99bbpqf`%wI0_p9DDU%EYa8>X9?>_VZK z^LC*reJK_Z0kA4tEvS6XTR!b($lB&NB&W%W3FVkacpZvj1wG|$?*?V5$hO96LT&t{ z#Vr`~>B&WXR0%h4T<=P2-ZX#LxHzG0aj+DhL6Mc;ZrAG%Um{)y91 zR~Rzc>dkOlyQwlG9r5XkV?zI(|mbL!RHX~KOV zvU=GFqDKS){Zl#9m-ijvn5E9u{R00R6a$E|e2KQTOnQWLHOBV?K`WSgaYez+uOybO zHT2Vs4vWf!v=UCu-~nO|gBNqGhtZF$!%1IH$QVEP_w#FATX$Mb$TNcTn{?@b`AkMSX6r)iE`&xVWeofh-z0GKLD0n0`vi zDBDPu0c9) zW#{I}O<&jgxEvh7eRUW{IiBYD<9)MBuYk4clbOd;pPTThVQ4fJHd%aA6sy|nTMMqt zR9RDZzp8=+t<^mZ(_j&aIhW>v?9Yw_O=-K8lfJ+3*k6(TCG1n_IJkC)8jiN6Gn<^} z{V_vEyE_VjAS_}3rX8Dz?JGkj9`gEe??iCW#zVi~@zdk6M+HgOOsB`T5-D0H89@}G zJDUUh1Rx5d2h}|VQjN^A_Am9kBkziPI!uc4lP$1azZu<{f7UV-wfBPzP*6}{i+=su zHbW|r^&nHiXG?4=W!ZVz`{@6A0gO7{$&x#HHRLpqoA9*Z>)qFgW;b&1?mf))9oz7N zPGxASI(6W_@g6QO0Hl5HgL`u^nekmPqeM(Yd1RouG_#Nsx{6^an4ho?ZQ zK6y=l$zOWWD&M2^#JaElsy!>LTa8QS&!g3VjhkgP=8i6jLO7PwAv*RW%5o&ZvH+;agy|708|A|Nk5MuQ10}p`5--mj zP4Mqd_iM-JW^XfPeka(6AN;JkU&gnj+0l7rEf4~NlE})6(s;Z`?qHI&rZ#C7V6tW} z(6{G(&6b*)+AP-axi}6gnU02LE?e-6*!>r}uQR9tiPK+y!PvVxid9(JE~({>=96%{ z1DKUMYr9g@GB7;%-(9?l%kD6ptM0}RW`%RkZ^;$riWVbYnUrJW3|{<~>qb{c=4 z7oTe{;H(PG)VR0tM_ww)I`e8}QL~NZUI= z3IIx3VR?!uj4s$U9fp_|y+fXZI@ zFyjB3*21xQu^hNWUJF`AE2RV8K0f{uIH>OS-(goTU3uFYzQSEeJoYv=5VM~rSZhU( zw>g?P7uy0oE6b&tpwbWs0-=iSdi~AlLq9}IK!Y_wRK@a{p48Rg+rf>WBW4N~rqb`6 zzO_TfUeoQC@i~zNelDr{QUo_g1F&RT)Y!&^&Ze%+ zKuKw7TPDP91p`T>TM&25NKJRXGZ%OSLnc^2Q$k>+Jp@DRNVugyNS85`+^KY9 zo02eYoBH|5{rh)6j1;0Q<>DhiV^QjxSePNA{hMl8IpT zQs}fa8eu(4<5v?-d+nFIPktgLD8AVC781UP@`~$~^2Nrk|#2a@B zc@O|37;c2eh*ccu-1|3mU+7P#$G_iPq-yxjejg?ns{dc7BGg?7ht|!58G{Enn0IQu z;ZDgfcz>9GQ|k0t4irg$BtQl<%YAc){q}?~i2FM@C@)9@f*?2Kg@1!+t2EKmS_t@% z^*^)av$@fsgT0hT)9sdxdPex*We*4nmr*Ky$$k+7ol`5GJt#^c5E7QDpbSLdH(29e_Pi|N7<8ul*zxwZES8yQS zjWrKz>i*Yv10lB(o0n-7kg2mbLIgaJSeVCrtgRJtl-$#RB8B_n12bQU6vr*SXZN@Y zbaaGk&`+;5r!!r*m0}YGEBo}QeFn`?0JaY%fGbTX0;mBvlbT79*8Tri3JfWCF=PBY zsafwQgoEmiY3V|1P3U~9KPCfgZEaNwiSmtoR<^LA_vw7}q;dQz8PM!9!8RaD;O6F# zy0pmjEFE|LJB}2B-2rOM0wGAdOG5&U{@0s?obvxSA7iULNsR$fDmtVZr6=}yXmLh! ztY|l9aBy(qUrNsMpt6_`mN~N`ed<+j>*MXcJ6s5veC0n$i!E7G8#fJp6k@bmk8zt3~d zbDqC<&hGoV?kitcylqr+3saI7b#g9u;Kio_m&26{F2YJ6Tpp}auMnOg$(Pt~yXrD|#&iVIO=BKcLo^l{%I^6%$#pcO= z*GZ)>(}{kJz>>DgPTD}t5Bhw-elG%@53i(BHSu@GwGT!x5<{fF+guzkfWvB{F1I3vF<~9-FF!^U10zVob+rjT!k64E?fK8Q zq|gLuaHa*<_iOY-u%@V7*V$C4yknR7H25GX2%0Th>JD zvBiCp>=0tzOHiIH$>+-{8(9z!=Y3sml7J_Ipk3x?v+~F!VXDK!!*UUz z=ic|+q?M|Ry%zvxAfcN4B(o!XZU51mf#Z1A^tFE8?h#rCU0BtM7f&Pt#q8%3lM?!ei@}|dt$I^$l&F4f^$bxu##Mvw$x9X%nxzYnTij&hD zz~}Dn(=Y6?!1kS2eSLkxMYC%|<&r&BhIhQ8&nN zHz_ZLIon?t=ZxM&)B~j|%sgJ47)BM10g8<{P=}`7-DNbYrfh)Yg zBld>S(<87Tc>;FCjiq%-ZgHIo3nC#qW@K=l8xeNn$C@O{z{Ndui7IgV-I{(l>{R!e z^oQiTx7>0;;}R$|G!u(8N2==q^m+WxQX+wq%>2(iZFnVKi1iZ!D9#aRaDf3?o7ZjF znFqL&)6!OC6aTOn4rYqKZZs(}QmLnfc0u`n^<@IfZNy3``2g^8s8JM28?PS3frK(=IBG*c1*u#q^X_-KHJ^bu1hT21%`< z;}qW7@q+wjW6@CE*#Dg7`@g5LClTyq&TTXJU;YG!TYW#Rh%-vxnv}b>og3J#vdX^# zV{Sb)1S;#lgBYEuQ&wM2$jJ#lEz?)*roH;+liz{-E@XM*2WkZh@`eoC3J4e%rKJK2 zy0Oa-A<64t6mDba!pq(!K~i1^+LMI6F-8>KS=0(6Fmk3pe{R4?evKq#wXyx}@1oKc zHNx?NCm8GUP^sdJ1HYR)KHWsMi#m-TNVJ!=U_attZTm=0a- z!RreHk-!BiJn<%UjDX)56(AG{+#>ta=SvcutjY*l32cR7KYjXyr8xWW;~>}#(EV`H z@wDhhKM-(LaaTG^1qMly@LT;?4FjP+}HlGK($)QaxM*;e7r>0vll;nJl)=@)Wl8B|iEkymgLA;ta%W}U! z;oZ*jzg+ViKsu=9&%EFxO&uNC-^a?y%3<_?b*3OirWSNO*5s#|nHk$FP<6Z6i!Fd% z?ind{0;oHmvC^0g51`;oVBOCV$(oVO`e6J73y&}dxThl`Su^*lK3G>4=)tI?qkuA^ zS)1LamX=nhOA1+YTl0~L=H&HP#l^)zDRm@#fF@&{)SA?*($Z4Dq8Xn1NjD4t)~uJy z!<%l1`q)cL%bo<#RUvsskYqQ(a-0x}WH=P@e`%xT1*z|l(Q!lEw6Us35?=tXv6Jw{ z!_Cdj?sN?_+j4NlGo|Wm$+-kQpppepe_{{Pc3HnTKmBV2IOxBmT8}(EjJO^LlgONP z@^3Ju*rO!JPM5S^KP zt;b)NEW-2zh0!Y4-fgNoK5c2B%>;_yz4P`5=e4xl>%)2N3p}O+=904EthJvtp>VJS z3ac%QlE?Cc)NgC42Sm9xSETg~do`uOjx1gn z34wxq5m0VyJ`c9hMq^e)69}cLJVI#WHT3{0#R3HQ7gwe9%~uctZ*%G@DQi_WBarT~ zCi^W~^!C#p8o*i(1I9{LL9n-EaKHNkzOSw6%ftB+Sg$jVThSC6IEb*UKI4 z?RDjUE23pTEYK8swcY|FnWMPK6yltQUMV}7qtHodZgf>|1OG5=OiA9wPau@$XhtLe z9X|+*8k9K^pesZ2i~!N8Ky)Bnu$B^ZAWw)_V@9NINa8=BrPQ%-FEa4bt7j!g$M#pg z0GR!IyC40xC33{+;_JCGAK3_ike0UykIV+9dY%AvJ(loqsp8xNkdM#N?@Pz&cc>&gdx5^>-l|Clt6@kG=yr>fSutHjY(o$JQNtQ zo!GU9kl}UUH?y6l?g5qEG$H|>Oo9`k;|2Jt>WRiV-wAg=>nC-u%x)6riI{uSl)OM{ zT3XZ*0|RI&PN@{0mrinj)VXmBa8vf1k6XAYg8<{vNXRe}La)w@pd%Gy4$$*3H(6>0YnGg)St{Dg{U&GS@Nyudhv9viAx9=_HxB(PpJzi`PiE-w~{>BW$P)KfYyEu7! zY_b3hxq?Jybt2GY%Ib8ZF@5=(DF1)DH_2UTQ4IlX!OIzu%{?CGHI50cMO^i@wGhT4 zN`Bzmw{H=*-PW;f5g|su{TZ%Yk*#cp-yQ^KN`$=;t!LL!eP#vx<^8o2x+MqnBGm~n zD72gXAC*{og=cPJv>?A>N~xG06i-kXB#(zloq&S64T$*1^5VWMcM^kmKa)A8<0CsC z>lhdqWbug0I!i`|8jEHF7=rwD5Zo>AC#|6=^EYhksZq16L$(qONcyG1+y)@w3cuh5`L{4LyC%_36riCjW3QObpr)ZS8vzI|~mX^E1ug}bVALlZt z2Cg=Be*1L(%R52?@PrXO{Q^EKi$&2~$uW z1=O-VB)OOb^_3GD4~ea|A)}1VVX5PiSmwShudom8&p-1yUzR$ z+5B4}Nu0_NPsXC?fznDKm(O!+G0qfn(t;fr9L(^+9PDIJb&ww%u4b%ku>IkP$}l}S z{ML|&iiF~@P~?z^di%ux@2O~vO+Xy|0KBfvT@py-P~=;^;n_yx`qld*(vvOA3yk%x z0aF~d3H<4PN_u*6{H3MdeXh>h6s`8EQ=S5S{i>?I_v+5rXx~FQABFB2?Nom}fLbQ` zXZIV#{>cyaway(`B$nzC9v}QfjEl!QqZkjMF#O;%<7X~p1nhN~SnPS@DrVoNclLv; z@4~mF$)w`q;=GmLd2~QN1NbvI)X4W+o1>hrJSDo$0bqMagQbsAoza9sxv-iXP?%Y? z7#EK|czFXcK@CNcaH77xx_rdvy#G`qitsh{nn1a)<4&ZVPgMUTvF339c2}VJ5~dbI z3HWfR@dD<{KYT-RH*hLsoJAAkDfNj}3C?KRMFnU!o38#vS!+h>WqkN(L?All0Z zuq(s-4}aOKJG4F9;enKD0dWd&IuH8x*kmt@0y;2_Hu@1ecRUpK=T0W#%0D$Xe}Z+} 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�{mz(P}o3jcmD=(jieek1J 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 literal 0 HcmV?d00001 diff --git a/tools/nitpick/icon/nitpick.ico b/tools/nitpick/icon/nitpick.ico new file mode 100644 index 0000000000000000000000000000000000000000..e3d852cb41008ddd5d28970d58bee35a2666d71f GIT binary patch literal 52562 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 D)Fem? literal 0 HcmV?d00001 diff --git a/tools/nitpick/src/BusyWindow.cpp b/tools/nitpick/src/BusyWindow.cpp new file mode 100644 index 0000000000..8066d576f8 --- /dev/null +++ b/tools/nitpick/src/BusyWindow.cpp @@ -0,0 +1,14 @@ +// +// BusyWindow.cpp +// +// Created by Nissim Hadar on 26 Jul 2017. +// Copyright 2013 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 "BusyWindow.h" + +BusyWindow::BusyWindow(QWidget *parent) { + setupUi(this); +} diff --git a/tools/nitpick/src/BusyWindow.h b/tools/nitpick/src/BusyWindow.h new file mode 100644 index 0000000000..62f2df7e04 --- /dev/null +++ b/tools/nitpick/src/BusyWindow.h @@ -0,0 +1,22 @@ +// +// BusyWindow.h +// +// Created by Nissim Hadar on 29 Jul 2017. +// Copyright 2013 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_BusyWindow_h +#define hifi_BusyWindow_h + +#include "ui_BusyWindow.h" + +class BusyWindow : public QDialog, public Ui::BusyWindow { + Q_OBJECT + +public: + BusyWindow(QWidget* parent = Q_NULLPTR); +}; + +#endif \ No newline at end of file diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp new file mode 100644 index 0000000000..58189b4795 --- /dev/null +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -0,0 +1,101 @@ +// +// MismatchWindow.cpp +// +// Created by Nissim Hadar on 9 Nov 2017. +// Copyright 2013 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 "MismatchWindow.h" + +#include + +#include + +MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { + setupUi(this); + + expectedImage->setScaledContents(true); + resultImage->setScaledContents(true); + diffImage->setScaledContents(true); +} + +QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { + // Create an empty difference image if the images differ in size + if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { + return QPixmap(); + } + + // This is an optimization, as QImage.setPixel() is embarrassingly slow + unsigned char* buffer = new unsigned char[expectedImage.height() * expectedImage.width() * 3]; + + // loop over each pixel + for (int y = 0; y < expectedImage.height(); ++y) { + for (int x = 0; x < expectedImage.width(); ++x) { + QRgb pixelP = expectedImage.pixel(QPoint(x, y)); + QRgb pixelQ = resultImage.pixel(QPoint(x, y)); + + // Convert to luminance + double p = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); + double q = R_Y * qRed(pixelQ) + G_Y * qGreen(pixelQ) + B_Y * qBlue(pixelQ); + + // The intensity value is modified to increase the brightness of the displayed image + double absoluteDifference = fabs(p - q) / 255.0; + double modifiedDifference = sqrt(absoluteDifference); + + int difference = (int)(modifiedDifference * 255.0); + + buffer[3 * (x + y * expectedImage.width()) + 0] = difference; + buffer[3 * (x + y * expectedImage.width()) + 1] = difference; + buffer[3 * (x + y * expectedImage.width()) + 2] = difference; + } + } + + QImage diffImage(buffer, expectedImage.width(), expectedImage.height(), QImage::Format_RGB888); + QPixmap resultPixmap = QPixmap::fromImage(diffImage); + + delete[] buffer; + + return resultPixmap; +} + +void MismatchWindow::setTestResult(TestResult testResult) { + errorLabel->setText("Similarity: " + QString::number(testResult._error)); + + imagePath->setText("Path to test: " + testResult._pathname); + + expectedFilename->setText(testResult._expectedImageFilename); + resultFilename->setText(testResult._actualImageFilename); + + QPixmap expectedPixmap = QPixmap(testResult._pathname + testResult._expectedImageFilename); + QPixmap actualPixmap = QPixmap(testResult._pathname + testResult._actualImageFilename); + + _diffPixmap = computeDiffPixmap( + QImage(testResult._pathname + testResult._expectedImageFilename), + QImage(testResult._pathname + testResult._actualImageFilename) + ); + + expectedImage->setPixmap(expectedPixmap); + resultImage->setPixmap(actualPixmap); + diffImage->setPixmap(_diffPixmap); +} + +void MismatchWindow::on_passTestButton_clicked() { + _userResponse = USER_RESPONSE_PASS; + close(); +} + +void MismatchWindow::on_failTestButton_clicked() { + _userResponse = USE_RESPONSE_FAIL; + close(); +} + +void MismatchWindow::on_abortTestsButton_clicked() { + _userResponse = USER_RESPONSE_ABORT; + close(); +} + +QPixmap MismatchWindow::getComparisonImage() { + return _diffPixmap; +} diff --git a/tools/nitpick/src/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h new file mode 100644 index 0000000000..040e0b8bf1 --- /dev/null +++ b/tools/nitpick/src/MismatchWindow.h @@ -0,0 +1,42 @@ +// +// MismatchWindow.h +// +// Created by Nissim Hadar on 9 Nov 2017. +// Copyright 2013 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_MismatchWindow_h +#define hifi_MismatchWindow_h + +#include "ui_MismatchWindow.h" + +#include "common.h" + +class MismatchWindow : public QDialog, public Ui::MismatchWindow { + Q_OBJECT + +public: + MismatchWindow(QWidget *parent = Q_NULLPTR); + + void setTestResult(TestResult testResult); + + UserResponse getUserResponse() { return _userResponse; } + + QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); + QPixmap getComparisonImage(); + +private slots: + void on_passTestButton_clicked(); + void on_failTestButton_clicked(); + void on_abortTestsButton_clicked(); + +private: + UserResponse _userResponse{ USER_RESPONSE_INVALID }; + + QPixmap _diffPixmap; +}; + + +#endif // hifi_MismatchWindow_h \ No newline at end of file diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp new file mode 100644 index 0000000000..9e385bcd4d --- /dev/null +++ b/tools/nitpick/src/Nitpick.cpp @@ -0,0 +1,372 @@ +// +// Nitpick.cpp +// zone/ambientLightInheritence +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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 "Nitpick.h" + +#ifdef Q_OS_WIN +#include +#include +#endif + +#include + +Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { + _ui.setupUi(this); + + _ui.checkBoxInteractiveMode->setChecked(true); + _ui.progressBar->setVisible(false); + _ui.tabWidget->setCurrentIndex(0); + + _signalMapper = new QSignalMapper(); + + connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked); + connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); + connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); + + // The second tab hides and shows the Windows task bar +#ifndef Q_OS_WIN + _ui.tabWidget->removeTab(1); +#endif + + _ui.statusLabelOnDesktop->setText(""); + _ui.statusLabelOnMobile->setText(""); + + _ui.plainTextEdit->setReadOnly(true); + + setWindowTitle("Nitpick - v2.0.1"); +} + +Nitpick::~Nitpick() { + delete _signalMapper; + + if (_test) { + delete _test; + } + + if (_testRunnerDesktop) { + delete _testRunnerDesktop; + } + + if (_testRunnerMobile) { + delete _testRunnerMobile; + } +} + +void Nitpick::setup() { + if (_test) { + delete _test; + } + _test = new Test(_ui.progressBar, _ui.checkBoxInteractiveMode); + + std::vector dayCheckboxes; + dayCheckboxes.emplace_back(_ui.mondayCheckBox); + dayCheckboxes.emplace_back(_ui.tuesdayCheckBox); + dayCheckboxes.emplace_back(_ui.wednesdayCheckBox); + dayCheckboxes.emplace_back(_ui.thursdayCheckBox); + dayCheckboxes.emplace_back(_ui.fridayCheckBox); + dayCheckboxes.emplace_back(_ui.saturdayCheckBox); + dayCheckboxes.emplace_back(_ui.sundayCheckBox); + + std::vector timeEditCheckboxes; + timeEditCheckboxes.emplace_back(_ui.timeEdit1checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit2checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit3checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit4checkBox); + + std::vector timeEdits; + timeEdits.emplace_back(_ui.timeEdit1); + timeEdits.emplace_back(_ui.timeEdit2); + timeEdits.emplace_back(_ui.timeEdit3); + timeEdits.emplace_back(_ui.timeEdit4); + + // Create the two test runners + if (_testRunnerDesktop) { + delete _testRunnerDesktop; + } + _testRunnerDesktop = new TestRunnerDesktop( + dayCheckboxes, + timeEditCheckboxes, + timeEdits, + _ui.workingFolderRunOnDesktopLabel, + _ui.checkBoxServerless, + _ui.runLatestOnDesktopCheckBox, + _ui.urlOnDesktopLineEdit, + _ui.runNowPushbutton, + _ui.statusLabelOnDesktop + ); + + if (_testRunnerMobile) { + delete _testRunnerMobile; + } + _testRunnerMobile = new TestRunnerMobile( + _ui.workingFolderRunOnMobileLabel, + _ui.connectDevicePushbutton, + _ui.pullFolderPushbutton, + _ui.detectedDeviceLabel, + _ui.folderLineEdit, + _ui.downloadAPKPushbutton, + _ui.installAPKPushbutton, + _ui.runLatestOnMobileCheckBox, + _ui.urlOnMobileLineEdit, + _ui.statusLabelOnMobile + ); +} + +void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory, + const QString& branch, + const QString& user +) { + _test->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); +} + +void Nitpick::on_tabWidget_currentChanged(int index) { +// Enable the GitHub edit boxes as required +#ifdef Q_OS_WIN + if (index == 0 || index == 2 || index == 3 || index == 4) { +#else + if (index == 0 || index == 1 || index == 2 || index == 3) { +#endif + _ui.userLineEdit->setDisabled(false); + _ui.branchLineEdit->setDisabled(false); + } else { + _ui.userLineEdit->setDisabled(true); + _ui.branchLineEdit->setDisabled(true); + } +} + +void Nitpick::on_evaluateTestsPushbutton_clicked() { + _test->startTestsEvaluation(false, false); +} + +void Nitpick::on_createRecursiveScriptPushbutton_clicked() { + _test->createRecursiveScript(); +} + +void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { + _test->createAllRecursiveScripts(); +} + +void Nitpick::on_createTestsPushbutton_clicked() { + _test->createTests(); +} + +void Nitpick::on_createMDFilePushbutton_clicked() { + _test->createMDFile(); +} + +void Nitpick::on_createAllMDFilesPushbutton_clicked() { + _test->createAllMDFiles(); +} + +void Nitpick::on_createTestAutoScriptPushbutton_clicked() { + _test->createTestAutoScript(); +} + +void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() { + _test->createAllTestAutoScripts(); +} + +void Nitpick::on_createTestsOutlinePushbutton_clicked() { + _test->createTestsOutline(); +} + +void Nitpick::on_createTestRailTestCasesPushbutton_clicked() { + _test->createTestRailTestCases(); +} + +void Nitpick::on_createTestRailRunButton_clicked() { + _test->createTestRailRun(); +} + +void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() { + _testRunnerDesktop->setWorkingFolderAndEnableControls(); +} + +void Nitpick::enableRunTabControls() { + _ui.runNowPushbutton->setEnabled(true); + _ui.daysGroupBox->setEnabled(true); + _ui.timesGroupBox->setEnabled(true); +} + +void Nitpick::on_runNowPushbutton_clicked() { + _testRunnerDesktop->run(); +} + +void Nitpick::on_runLatestOnDesktopCheckBox_clicked() { + _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked()); +} + +void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { + _testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); +} + +void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() { + _test->updateTestRailRunResult(); +} + +// To toggle between show and hide +// if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked(); +// else on_hideTaskbarButton_clicked(); +// +void Nitpick::on_hideTaskbarPushbutton_clicked() { +#ifdef Q_OS_WIN + APPBARDATA abd = { sizeof abd }; + UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); + LPARAM param = uState & ABS_ALWAYSONTOP; + abd.lParam = ABS_AUTOHIDE | param; + SHAppBarMessage(ABM_SETSTATE, &abd); +#endif +} + +void Nitpick::on_showTaskbarPushbutton_clicked() { +#ifdef Q_OS_WIN + APPBARDATA abd = { sizeof abd }; + UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); + LPARAM param = uState & ABS_ALWAYSONTOP; + abd.lParam = param; + SHAppBarMessage(ABM_SETSTATE, &abd); +#endif +} + +void Nitpick::on_closePushbutton_clicked() { + exit(0); +} + +void Nitpick::on_createPythonScriptRadioButton_clicked() { + _test->setTestRailCreateMode(PYTHON); +} + +void Nitpick::on_createXMLScriptRadioButton_clicked() { + _test->setTestRailCreateMode(XML); +} + +void Nitpick::on_createWebPagePushbutton_clicked() { + _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); +} + +void Nitpick::downloadFile(const QUrl& url) { + _downloaders.emplace_back(new Downloader(url, this)); + connect(_downloaders[_index], SIGNAL(downloaded()), _signalMapper, SLOT(map())); + + _signalMapper->setMapping(_downloaders[_index], _index); + + ++_index; +} + +void Nitpick::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { + connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); + + _directoryName = directoryName; + _filenames = filenames; + _caller = caller; + + _numberOfFilesToDownload = URLs.size(); + _numberOfFilesDownloaded = 0; + _index = 0; + + _ui.progressBar->setMinimum(0); + _ui.progressBar->setMaximum(_numberOfFilesToDownload - 1); + _ui.progressBar->setValue(0); + _ui.progressBar->setVisible(true); + + foreach (auto downloader, _downloaders) { + delete downloader; + } + + _downloaders.clear(); + for (int i = 0; i < _numberOfFilesToDownload; ++i) { + downloadFile(URLs[i]); + } +} + +void Nitpick::saveFile(int index) { + try { + QFile file(_directoryName + "/" + _filenames[index]); + file.open(QIODevice::WriteOnly); + file.write(_downloaders[index]->downloadedData()); + file.close(); + } catch (...) { + QMessageBox::information(0, "Test Aborted", "Failed to save file: " + _filenames[index]); + _ui.progressBar->setVisible(false); + return; + } + + ++_numberOfFilesDownloaded; + + if (_numberOfFilesDownloaded == _numberOfFilesToDownload) { + disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); + if (_caller == _test) { + _test->finishTestsEvaluation(); + } else if (_caller == _testRunnerDesktop) { + _testRunnerDesktop->downloadComplete(); + } else if (_caller == _testRunnerMobile) { + _testRunnerMobile->downloadComplete(); + } + + _ui.progressBar->setVisible(false); + } else { + _ui.progressBar->setValue(_numberOfFilesDownloaded); + } +} + +void Nitpick::about() { + QMessageBox::information(0, "About", QString("Built ") + __DATE__ + ", " + __TIME__); +} + +void Nitpick::content() { + QDesktopServices::openUrl(QUrl("https://github.com/highfidelity/hifi/blob/master/tools/nitpick/README.md")); +} + +void Nitpick::setUserText(const QString& user) { + _ui.userLineEdit->setText(user); +} + +QString Nitpick::getSelectedUser() { + return _ui.userLineEdit->text(); +} + +void Nitpick::setBranchText(const QString& branch) { + _ui.branchLineEdit->setText(branch); +} + +QString Nitpick::getSelectedBranch() { + return _ui.branchLineEdit->text(); +} + +void Nitpick::appendLogWindow(const QString& message) { + _ui.plainTextEdit->appendPlainText(message); +} + +// Test on Mobile +void Nitpick::on_setWorkingFolderRunOnMobilePushbutton_clicked() { + _testRunnerMobile->setWorkingFolderAndEnableControls(); +} + +void Nitpick::on_connectDevicePushbutton_clicked() { + _testRunnerMobile->connectDevice(); +} + +void Nitpick::on_runLatestOnMobileCheckBox_clicked() { + _ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked()); +} + +void Nitpick::on_downloadAPKPushbutton_clicked() { + _testRunnerMobile->downloadAPK(); +} + +void Nitpick::on_installAPKPushbutton_clicked() { + _testRunnerMobile->installAPK(); +} + +void Nitpick::on_pullFolderPushbutton_clicked() { + _testRunnerMobile->pullFolder(); +} diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h new file mode 100644 index 0000000000..00516d1e76 --- /dev/null +++ b/tools/nitpick/src/Nitpick.h @@ -0,0 +1,134 @@ +// +// Nitpick.h +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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_Nitpick_h +#define hifi_Nitpick_h + +#include +#include +#include +#include "ui_Nitpick.h" + +#include "Downloader.h" +#include "Test.h" + +#include "TestRunnerDesktop.h" +#include "TestRunnerMobile.h" + +#include "AWSInterface.h" + +class Nitpick : public QMainWindow { + Q_OBJECT + +public: + Nitpick(QWidget* parent = Q_NULLPTR); + ~Nitpick(); + + void setup(); + + void startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory, + const QString& branch, + const QString& user); + + void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); + + void downloadFile(const QUrl& url); + void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void* caller); + + void setUserText(const QString& user); + QString getSelectedUser(); + + void setBranchText(const QString& branch); + QString getSelectedBranch(); + + void enableRunTabControls(); + + void appendLogWindow(const QString& message); + +private slots: + void on_closePushbutton_clicked(); + + void on_tabWidget_currentChanged(int index); + + void on_evaluateTestsPushbutton_clicked(); + void on_createRecursiveScriptPushbutton_clicked(); + void on_createAllRecursiveScriptsPushbutton_clicked(); + void on_createTestsPushbutton_clicked(); + + void on_createMDFilePushbutton_clicked(); + void on_createAllMDFilesPushbutton_clicked(); + + void on_createTestAutoScriptPushbutton_clicked(); + void on_createAllTestAutoScriptsPushbutton_clicked(); + + void on_createTestsOutlinePushbutton_clicked(); + + void on_createTestRailTestCasesPushbutton_clicked(); + void on_createTestRailRunButton_clicked(); + + void on_setWorkingFolderRunOnDesktopPushbutton_clicked(); + void on_runNowPushbutton_clicked(); + + void on_runLatestOnDesktopCheckBox_clicked(); + + void on_updateTestRailRunResultsPushbutton_clicked(); + + void on_hideTaskbarPushbutton_clicked(); + void on_showTaskbarPushbutton_clicked(); + + void on_createPythonScriptRadioButton_clicked(); + void on_createXMLScriptRadioButton_clicked(); + + void on_createWebPagePushbutton_clicked(); + + void saveFile(int index); + + void about(); + void content(); + + // Run on Mobile controls + void on_setWorkingFolderRunOnMobilePushbutton_clicked(); + void on_connectDevicePushbutton_clicked(); + void on_runLatestOnMobileCheckBox_clicked(); + + void on_downloadAPKPushbutton_clicked(); + void on_installAPKPushbutton_clicked(); + + void on_pullFolderPushbutton_clicked(); + +private: + Ui::NitpickClass _ui; + Test* _test{ nullptr }; + + TestRunnerDesktop* _testRunnerDesktop{ nullptr }; + TestRunnerMobile* _testRunnerMobile{ nullptr }; + + AWSInterface _awsInterface; + + std::vector _downloaders; + + // local storage for parameters - folder to store downloaded files in, and a list of their names + QString _directoryName; + QStringList _filenames; + + // Used to enable passing a parameter to slots + QSignalMapper* _signalMapper; + + int _numberOfFilesToDownload{ 0 }; + int _numberOfFilesDownloaded{ 0 }; + int _index{ 0 }; + + bool _isRunningFromCommandline{ false }; + + void* _caller; +}; + +#endif // hifi_Nitpick_h \ No newline at end of file diff --git a/tools/nitpick/src/TestRailResultsSelectorWindow.cpp b/tools/nitpick/src/TestRailResultsSelectorWindow.cpp new file mode 100644 index 0000000000..505e04b33e --- /dev/null +++ b/tools/nitpick/src/TestRailResultsSelectorWindow.cpp @@ -0,0 +1,106 @@ +// +// TestRailResultsSelectorWindow.cpp +// +// Created by Nissim Hadar on 2 Aug 2017. +// Copyright 2013 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 "TestRailResultsSelectorWindow.h" + +#include + +#include + +TestRailResultsSelectorWindow::TestRailResultsSelectorWindow(QWidget *parent) { + setupUi(this); + + projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); +} + + +void TestRailResultsSelectorWindow::reset() { + urlLineEdit->setDisabled(false); + userLineEdit->setDisabled(false); + passwordLineEdit->setDisabled(false); + projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); + + OKButton->setDisabled(true); + + runsLabel->setDisabled(true); + runsComboBox->setDisabled(true); +} + +void TestRailResultsSelectorWindow::on_acceptButton_clicked() { + urlLineEdit->setDisabled(true); + userLineEdit->setDisabled(true); + passwordLineEdit->setDisabled(true); + projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); + + OKButton->setDisabled(false); + + runsLabel->setDisabled(false); + runsComboBox->setDisabled(false); + close(); +} + +void TestRailResultsSelectorWindow::on_OKButton_clicked() { + userCancelled = false; + close(); +} + +void TestRailResultsSelectorWindow::on_cancelButton_clicked() { + userCancelled = true; + close(); +} + +bool TestRailResultsSelectorWindow::getUserCancelled() { + return userCancelled; +} + +void TestRailResultsSelectorWindow::setURL(const QString& user) { + urlLineEdit->setText(user); +} + +QString TestRailResultsSelectorWindow::getURL() { + return urlLineEdit->text(); +} + +void TestRailResultsSelectorWindow::setUser(const QString& user) { + userLineEdit->setText(user); +} + +QString TestRailResultsSelectorWindow::getUser() { + return userLineEdit->text(); +} + +QString TestRailResultsSelectorWindow::getPassword() { + return passwordLineEdit->text(); +} + +void TestRailResultsSelectorWindow::setProjectID(const int project) { + projectIDLineEdit->setText(QString::number(project)); +} + +int TestRailResultsSelectorWindow::getProjectID() { + return projectIDLineEdit->text().toInt(); +} + +void TestRailResultsSelectorWindow::setSuiteID(const int project) { + suiteIDLineEdit->setText(QString::number(project)); +} + +int TestRailResultsSelectorWindow::getSuiteID() { + return suiteIDLineEdit->text().toInt(); +} + +void TestRailResultsSelectorWindow::updateRunsComboBoxData(QStringList data) { + runsComboBox->insertItems(0, data); +} + +int TestRailResultsSelectorWindow::getRunID() { + return runsComboBox->currentIndex(); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRailResultsSelectorWindow.h b/tools/nitpick/src/TestRailResultsSelectorWindow.h new file mode 100644 index 0000000000..51059d6127 --- /dev/null +++ b/tools/nitpick/src/TestRailResultsSelectorWindow.h @@ -0,0 +1,50 @@ +// +// TestRailResultsSelectorWindow.h +// +// Created by Nissim Hadar on 2 Aug 2017. +// Copyright 2013 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_TestRailResultsSelectorWindow_h +#define hifi_TestRailResultsSelectorWindow_h + +#include "ui_TestRailResultsSelectorWindow.h" + +class TestRailResultsSelectorWindow : public QDialog, public Ui::TestRailResultsSelectorWindow { + Q_OBJECT + +public: + TestRailResultsSelectorWindow(QWidget* parent = Q_NULLPTR); + + void reset(); + + bool getUserCancelled(); + + void setURL(const QString& user); + QString getURL(); + + void setUser(const QString& user); + QString getUser(); + + QString getPassword(); + + void setProjectID(const int project); + int getProjectID(); + + void setSuiteID(const int project); + int getSuiteID(); + + bool userCancelled{ false }; + + void updateRunsComboBoxData(QStringList data); + int getRunID(); + +private slots: + void on_acceptButton_clicked(); + void on_OKButton_clicked(); + void on_cancelButton_clicked(); +}; + +#endif \ No newline at end of file diff --git a/tools/nitpick/src/TestRailRunSelectorWindow.cpp b/tools/nitpick/src/TestRailRunSelectorWindow.cpp new file mode 100644 index 0000000000..ac3419d46f --- /dev/null +++ b/tools/nitpick/src/TestRailRunSelectorWindow.cpp @@ -0,0 +1,101 @@ +// +// TestRailRunSelectorWindow.cpp +// +// Created by Nissim Hadar on 31 Jul 2017. +// Copyright 2013 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 "TestRailRunSelectorWindow.h" + +#include + +#include + +TestRailRunSelectorWindow::TestRailRunSelectorWindow(QWidget *parent) { + setupUi(this); + + projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); +} + +void TestRailRunSelectorWindow::reset() { + urlLineEdit->setDisabled(false); + userLineEdit->setDisabled(false); + passwordLineEdit->setDisabled(false); + projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); + + OKButton->setDisabled(true); + sectionsComboBox->setDisabled(true); +} + +void TestRailRunSelectorWindow::on_acceptButton_clicked() { + urlLineEdit->setDisabled(true); + userLineEdit->setDisabled(true); + passwordLineEdit->setDisabled(true); + projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); + + OKButton->setDisabled(false); + sectionsComboBox->setDisabled(false); + close(); +} + +void TestRailRunSelectorWindow::on_OKButton_clicked() { + userCancelled = false; + close(); +} + +void TestRailRunSelectorWindow::on_cancelButton_clicked() { + userCancelled = true; + close(); +} + +bool TestRailRunSelectorWindow::getUserCancelled() { + return userCancelled; +} + +void TestRailRunSelectorWindow::setURL(const QString& user) { + urlLineEdit->setText(user); +} + +QString TestRailRunSelectorWindow::getURL() { + return urlLineEdit->text(); +} + +void TestRailRunSelectorWindow::setUser(const QString& user) { + userLineEdit->setText(user); +} + +QString TestRailRunSelectorWindow::getUser() { + return userLineEdit->text(); +} + +QString TestRailRunSelectorWindow::getPassword() { + return passwordLineEdit->text(); +} + +void TestRailRunSelectorWindow::setProjectID(const int project) { + projectIDLineEdit->setText(QString::number(project)); +} + +int TestRailRunSelectorWindow::getProjectID() { + return projectIDLineEdit->text().toInt(); +} + +void TestRailRunSelectorWindow::setSuiteID(const int project) { + suiteIDLineEdit->setText(QString::number(project)); +} + +int TestRailRunSelectorWindow::getSuiteID() { + return suiteIDLineEdit->text().toInt(); +} + +void TestRailRunSelectorWindow::updateSectionsComboBoxData(QStringList data) { + sectionsComboBox->insertItems(0, data); +} + +int TestRailRunSelectorWindow::getSectionID() { + return sectionsComboBox->currentIndex(); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRailRunSelectorWindow.h b/tools/nitpick/src/TestRailRunSelectorWindow.h new file mode 100644 index 0000000000..d6428bb476 --- /dev/null +++ b/tools/nitpick/src/TestRailRunSelectorWindow.h @@ -0,0 +1,50 @@ +// +// TestRailRunSelectorWindow.h +// +// Created by Nissim Hadar on 31 Jul 2017. +// Copyright 2013 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_TestRailRunSelectorWindow_h +#define hifi_TestRailRunSelectorWindow_h + +#include "ui_TestRailRunSelectorWindow.h" + +class TestRailRunSelectorWindow : public QDialog, public Ui::TestRailRunSelectorWindow { + Q_OBJECT + +public: + TestRailRunSelectorWindow(QWidget* parent = Q_NULLPTR); + + void reset(); + + bool getUserCancelled(); + + void setURL(const QString& user); + QString getURL(); + + void setUser(const QString& user); + QString getUser(); + + QString getPassword(); + + void setProjectID(const int project); + int getProjectID(); + + void setSuiteID(const int project); + int getSuiteID(); + + bool userCancelled{ false }; + + void updateSectionsComboBoxData(QStringList data); + int getSectionID(); + +private slots: + void on_acceptButton_clicked(); + void on_OKButton_clicked(); + void on_cancelButton_clicked(); +}; + +#endif \ No newline at end of file diff --git a/tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp b/tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp new file mode 100644 index 0000000000..638fe71819 --- /dev/null +++ b/tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp @@ -0,0 +1,106 @@ +// +// TestRailTestCasesSelectorWindow.cpp +// +// Created by Nissim Hadar on 26 Jul 2017. +// Copyright 2013 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 "TestRailTestCasesSelectorWindow.h" + +#include + +#include + +TestRailTestCasesSelectorWindow::TestRailTestCasesSelectorWindow(QWidget *parent) { + setupUi(this); + + projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); +} + + +void TestRailTestCasesSelectorWindow::reset() { + urlLineEdit->setDisabled(false); + userLineEdit->setDisabled(false); + passwordLineEdit->setDisabled(false); + projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); + + OKButton->setDisabled(true); + + releasesLabel->setDisabled(true); + releasesComboBox->setDisabled(true); +} + +void TestRailTestCasesSelectorWindow::on_acceptButton_clicked() { + urlLineEdit->setDisabled(true); + userLineEdit->setDisabled(true); + passwordLineEdit->setDisabled(true); + projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); + + OKButton->setDisabled(false); + + releasesLabel->setDisabled(false); + releasesComboBox->setDisabled(false); + close(); +} + +void TestRailTestCasesSelectorWindow::on_OKButton_clicked() { + userCancelled = false; + close(); +} + +void TestRailTestCasesSelectorWindow::on_cancelButton_clicked() { + userCancelled = true; + close(); +} + +bool TestRailTestCasesSelectorWindow::getUserCancelled() { + return userCancelled; +} + +void TestRailTestCasesSelectorWindow::setURL(const QString& user) { + urlLineEdit->setText(user); +} + +QString TestRailTestCasesSelectorWindow::getURL() { + return urlLineEdit->text(); +} + +void TestRailTestCasesSelectorWindow::setUser(const QString& user) { + userLineEdit->setText(user); +} + +QString TestRailTestCasesSelectorWindow::getUser() { + return userLineEdit->text(); +} + +QString TestRailTestCasesSelectorWindow::getPassword() { + return passwordLineEdit->text(); +} + +void TestRailTestCasesSelectorWindow::setProjectID(const int project) { + projectIDLineEdit->setText(QString::number(project)); +} + +int TestRailTestCasesSelectorWindow::getProjectID() { + return projectIDLineEdit->text().toInt(); +} + +void TestRailTestCasesSelectorWindow::setSuiteID(const int project) { + suiteIDLineEdit->setText(QString::number(project)); +} + +int TestRailTestCasesSelectorWindow::getSuiteID() { + return suiteIDLineEdit->text().toInt(); +} + +void TestRailTestCasesSelectorWindow::updateReleasesComboBoxData(QStringList data) { + releasesComboBox->insertItems(0, data); +} + +int TestRailTestCasesSelectorWindow::getReleaseID() { + return releasesComboBox->currentIndex(); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRailTestCasesSelectorWindow.h b/tools/nitpick/src/TestRailTestCasesSelectorWindow.h new file mode 100644 index 0000000000..9153b003fa --- /dev/null +++ b/tools/nitpick/src/TestRailTestCasesSelectorWindow.h @@ -0,0 +1,50 @@ +// +// TestRailTestCasesSelectorWindow.h +// +// Created by Nissim Hadar on 26 Jul 2017. +// Copyright 2013 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_TestRailTestCasesSelectorWindow_h +#define hifi_TestRailTestCasesSelectorWindow_h + +#include "ui_TestRailTestCasesSelectorWindow.h" + +class TestRailTestCasesSelectorWindow : public QDialog, public Ui::TestRailTestCasesSelectorWindow { + Q_OBJECT + +public: + TestRailTestCasesSelectorWindow(QWidget* parent = Q_NULLPTR); + + void reset(); + + bool getUserCancelled(); + + void setURL(const QString& user); + QString getURL(); + + void setUser(const QString& user); + QString getUser(); + + QString getPassword(); + + void setProjectID(const int project); + int getProjectID(); + + void setSuiteID(const int project); + int getSuiteID(); + + bool userCancelled{ false }; + + void updateReleasesComboBoxData(QStringList data); + int getReleaseID(); + +private slots: + void on_acceptButton_clicked(); + void on_OKButton_clicked(); + void on_cancelButton_clicked(); +}; + +#endif \ No newline at end of file diff --git a/tools/nitpick/ui/BusyWindow.ui b/tools/nitpick/ui/BusyWindow.ui new file mode 100644 index 0000000000..c237566a5e --- /dev/null +++ b/tools/nitpick/ui/BusyWindow.ui @@ -0,0 +1,75 @@ + + + BusyWindow + + + Qt::ApplicationModal + + + + 0 + 0 + 542 + 189 + + + + Updating TestRail - please wait + + + + + 30 + 850 + 500 + 28 + + + + + 12 + + + + similarity + + + + + + 40 + 40 + 481 + 101 + + + + 0 + + + 0 + + + + + + 50 + 60 + 431 + 61 + + + + + 20 + + + + Please wait for this window to close + + + + + + + diff --git a/tools/nitpick/ui/MismatchWindow.ui b/tools/nitpick/ui/MismatchWindow.ui new file mode 100644 index 0000000000..8a174989d4 --- /dev/null +++ b/tools/nitpick/ui/MismatchWindow.ui @@ -0,0 +1,199 @@ + + + MismatchWindow + + + Qt::ApplicationModal + + + + 0 + 0 + 1782 + 942 + + + + MismatchWindow + + + + + 10 + 25 + 800 + 450 + + + + expected image + + + + + + 900 + 25 + 800 + 450 + + + + result image + + + + + + 540 + 480 + 800 + 450 + + + + diff image + + + + + + 60 + 660 + 480 + 28 + + + + + 12 + + + + result image filename + + + + + + 60 + 630 + 480 + 28 + + + + + 12 + + + + expected image filename + + + + + + 20 + 600 + 1200 + 28 + + + + + 12 + + + + image path + + + + + + 30 + 790 + 75 + 23 + + + + Pass + + + + + + 120 + 790 + 75 + 23 + + + + Fail + + + + + + 210 + 790 + 121 + 23 + + + + Abort current test + + + + + + 30 + 850 + 500 + 28 + + + + + 12 + + + + similarity + + + + + + 30 + 5 + 151 + 16 + + + + Expected Image + + + + + + 930 + 5 + 151 + 16 + + + + Actual Image + + + + + + + diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui new file mode 100644 index 0000000000..8d69317369 --- /dev/null +++ b/tools/nitpick/ui/Nitpick.ui @@ -0,0 +1,1079 @@ + + + NitpickClass + + + + 0 + 0 + 720 + 870 + + + + + 0 + 0 + + + + Nitpick + + + + + + 470 + 750 + 100 + 40 + + + + Close + + + + + + 45 + 140 + 630 + 580 + + + + 0 + + + + Create + + + + + 210 + 60 + 220 + 40 + + + + Create Tests + + + + + + 70 + 180 + 220 + 40 + + + + Create MD file + + + + + + 320 + 180 + 220 + 40 + + + + Create all MD files + + + + + + 210 + 120 + 220 + 40 + + + + Create Tests Outline + + + + + + 70 + 300 + 220 + 40 + + + + Create Recursive Script + + + + + + 320 + 300 + 220 + 40 + + + + Create all Recursive Scripts + + + + + + 70 + 240 + 220 + 40 + + + + Create testAuto script + + + + + + 320 + 240 + 220 + 40 + + + + Create all testAuto scripts + + + + + + Windows + + + + + 200 + 130 + 211 + 40 + + + + Hide Windows Taskbar + + + + + + 200 + 200 + 211 + 40 + + + + Show Windows Taskbar + + + + + + Test on Desktop + + + + false + + + + 10 + 160 + 161 + 51 + + + + Run now + + + + + false + + + + 20 + 240 + 91 + 241 + + + + Days + + + + + 10 + 210 + 80 + 17 + + + + Sunday + + + + + + 10 + 90 + 80 + 17 + + + + Wednesday + + + + + + 10 + 60 + 80 + 17 + + + + Tuesday + + + + + + 10 + 120 + 80 + 17 + + + + Thursday + + + + + + 10 + 150 + 80 + 17 + + + + Friday + + + + + + 10 + 180 + 80 + 17 + + + + Saturday + + + + + + 10 + 30 + 80 + 17 + + + + Monday + + + + + + false + + + + 130 + 240 + 161 + 191 + + + + Times + + + + + 30 + 20 + 118 + 22 + + + + + + + 30 + 60 + 118 + 22 + + + + + + + 30 + 100 + 118 + 22 + + + + + + + 30 + 140 + 118 + 22 + + + + + + + 10 + 23 + 21 + 17 + + + + + + + + + + 10 + 63 + 21 + 17 + + + + + + + + + + 10 + 103 + 21 + 17 + + + + + + + + + + 10 + 143 + 21 + 17 + + + + + + + + + + + 10 + 20 + 160 + 30 + + + + Set Working Folder + + + + + + 190 + 20 + 320 + 30 + + + + (not set...) + + + + + + 300 + 210 + 311 + 331 + + + + + + + 300 + 170 + 41 + 31 + + + + Status: + + + + + + 350 + 170 + 271 + 31 + + + + ####### + + + + + + 20 + 70 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Server-less + + + false + + + + + + 20 + 100 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Latest + + + true + + + + + + 128 + 95 + 31 + 31 + + + + URL + + + + + false + + + + 170 + 100 + 451 + 21 + + + + + + + Test on Mobile + + + + false + + + + 10 + 90 + 160 + 30 + + + + Connect Device + + + + + + 190 + 96 + 320 + 30 + + + + (not detected) + + + + + + 10 + 20 + 160 + 30 + + + + Set Working Folder + + + + + + 190 + 20 + 320 + 30 + + + + (not set...) + + + + + false + + + + 460 + 350 + 160 + 30 + + + + Pull folder + + + + + false + + + + 10 + 350 + 440 + 30 + + + + + + false + + + + 170 + 170 + 451 + 21 + + + + + + + 20 + 170 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Latest + + + true + + + + + false + + + + 10 + 210 + 160 + 30 + + + + Download APK + + + + + + 290 + 20 + 41 + 31 + + + + Status: + + + + + + 340 + 20 + 271 + 31 + + + + ####### + + + + + false + + + + 10 + 250 + 160 + 30 + + + + Install APK + + + + + + Evaluate + + + + + 190 + 180 + 131 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Interactive Mode + + + + + + 330 + 170 + 181 + 51 + + + + Evaluate Test + + + + + + Web Interface + + + + + 240 + 220 + 160 + 40 + + + + Update Run Results + + + + + + 170 + 100 + 95 + 20 + + + + Python + + + true + + + + + + 240 + 160 + 160 + 40 + + + + Create Run + + + + + + 240 + 100 + 160 + 40 + + + + Create Test Cases + + + + + + 170 + 120 + 95 + 20 + + + + XML + + + + + + 10 + 30 + 601 + 300 + + + + TestRail + + + + + + 10 + 350 + 601 + 151 + + + + Amazon Web Services + + + + true + + + + 270 + 30 + 160 + 51 + + + + Create Web Page + + + + + + 150 + 42 + 111 + 17 + + + + Update AWS + + + + + + 20 + 90 + 561 + 21 + + + + + groupBox + updateTestRailRunResultsPushbutton + createPythonScriptRadioButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton + createXMLScriptRadioButton + groupBox_2 + + + + + + 120 + 80 + 110 + 16 + + + + + 10 + + + + GitHub Branch + + + + + + 120 + 40 + 110 + 16 + + + + + 10 + + + + GitHub User + + + + + + 80 + 760 + 255 + 23 + + + + 24 + + + + + + 220 + 40 + 161 + 21 + + + + + + + 220 + 80 + 161 + 21 + + + + + + + + 0 + 0 + 720 + 21 + + + + + File + + + + + + Help + + + + + + + + + + TopToolBarArea + + + false + + + + + + Close + + + + + About + + + + + Online readme + + + + + + userLineEdit + branchLineEdit + createTestsPushbutton + createMDFilePushbutton + createAllMDFilesPushbutton + createTestsOutlinePushbutton + createRecursiveScriptPushbutton + createAllRecursiveScriptsPushbutton + createTestAutoScriptPushbutton + createAllTestAutoScriptsPushbutton + hideTaskbarPushbutton + showTaskbarPushbutton + runNowPushbutton + sundayCheckBox + wednesdayCheckBox + tuesdayCheckBox + thursdayCheckBox + fridayCheckBox + saturdayCheckBox + mondayCheckBox + timeEdit1 + timeEdit2 + timeEdit3 + timeEdit4 + timeEdit1checkBox + timeEdit2checkBox + timeEdit3checkBox + timeEdit4checkBox + setWorkingFolderRunOnDesktopPushbutton + plainTextEdit + checkBoxServerless + runLatestOnDesktopCheckBox + urlOnDesktopLineEdit + checkBoxInteractiveMode + evaluateTestsPushbutton + updateTestRailRunResultsPushbutton + createPythonScriptRadioButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton + createXMLScriptRadioButton + createWebPagePushbutton + updateAWSCheckBox + awsURLLineEdit + closePushbutton + tabWidget + + + + diff --git a/tools/nitpick/ui/TestRailResultsSelectorWindow.ui b/tools/nitpick/ui/TestRailResultsSelectorWindow.ui new file mode 100644 index 0000000000..983b95ee79 --- /dev/null +++ b/tools/nitpick/ui/TestRailResultsSelectorWindow.ui @@ -0,0 +1,280 @@ + + + TestRailResultsSelectorWindow + + + + 0 + 0 + 533 + 474 + + + + TestRail Test Case Selector Window + + + + + 30 + 850 + 500 + 28 + + + + + 12 + + + + similarity + + + + + + 70 + 125 + 121 + 20 + + + + + 10 + + + + TestRail Password + + + + + + 70 + 25 + 121 + 20 + + + + + 10 + + + + TestRail URL + + + + + false + + + + 120 + 420 + 93 + 28 + + + + OK + + + + + + 280 + 420 + 93 + 28 + + + + Cancel + + + + + + 200 + 120 + 231 + 24 + + + + QLineEdit::Password + + + + + + 70 + 75 + 121 + 20 + + + + + 10 + + + + TestRail User + + + + + + 200 + 170 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 175 + 121 + 20 + + + + + 10 + + + + TestRail Project ID + + + + + + 200 + 270 + 231 + 28 + + + + Accept + + + + + false + + + + 160 + 350 + 271 + 22 + + + + + + true + + + + 80 + 350 + 71 + 20 + + + + + 10 + + + + TestRail Run + + + + + + 200 + 20 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 70 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 215 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 220 + 121 + 20 + + + + + 10 + + + + TestRail Suite ID + + + + + + urlLineEdit + userLineEdit + passwordLineEdit + projectIDLineEdit + suiteIDLineEdit + acceptButton + runsComboBox + OKButton + cancelButton + + + + diff --git a/tools/nitpick/ui/TestRailRunSelectorWindow.ui b/tools/nitpick/ui/TestRailRunSelectorWindow.ui new file mode 100644 index 0000000000..ad39b5cc64 --- /dev/null +++ b/tools/nitpick/ui/TestRailRunSelectorWindow.ui @@ -0,0 +1,283 @@ + + + TestRailRunSelectorWindow + + + Qt::ApplicationModal + + + + 0 + 0 + 489 + 474 + + + + TestRail Run Selector Window + + + + + 30 + 850 + 500 + 28 + + + + + 12 + + + + similarity + + + + + + 70 + 125 + 121 + 20 + + + + + 10 + + + + TestRail Password + + + + + + 70 + 25 + 121 + 20 + + + + + 10 + + + + TestRail URL + + + + + false + + + + 120 + 420 + 93 + 28 + + + + OK + + + + + + 280 + 420 + 93 + 28 + + + + Cancel + + + + + + 200 + 120 + 231 + 24 + + + + QLineEdit::Password + + + + + + 70 + 75 + 121 + 20 + + + + + 10 + + + + TestRail User + + + + + + 200 + 170 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 175 + 121 + 20 + + + + + 10 + + + + TestRail Project ID + + + + + + 200 + 270 + 231 + 28 + + + + Accept + + + + + false + + + + 140 + 350 + 311 + 22 + + + + + + true + + + + 20 + 350 + 121 + 20 + + + + + 10 + + + + TestRail Sections + + + + + + 200 + 20 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 70 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 215 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 220 + 121 + 20 + + + + + 10 + + + + TestRail Suite ID + + + + + + urlLineEdit + userLineEdit + passwordLineEdit + projectIDLineEdit + suiteIDLineEdit + acceptButton + sectionsComboBox + OKButton + cancelButton + + + + diff --git a/tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui b/tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui new file mode 100644 index 0000000000..41ff2943d5 --- /dev/null +++ b/tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui @@ -0,0 +1,280 @@ + + + TestRailTestCasesSelectorWindow + + + + 0 + 0 + 489 + 474 + + + + TestRail Test Case Selector Window + + + + + 30 + 850 + 500 + 28 + + + + + 12 + + + + similarity + + + + + + 70 + 125 + 121 + 20 + + + + + 10 + + + + TestRail Password + + + + + + 70 + 25 + 121 + 20 + + + + + 10 + + + + TestRail URL + + + + + false + + + + 120 + 420 + 93 + 28 + + + + OK + + + + + + 280 + 420 + 93 + 28 + + + + Cancel + + + + + + 200 + 120 + 231 + 24 + + + + QLineEdit::Password + + + + + + 70 + 75 + 121 + 20 + + + + + 10 + + + + TestRail User + + + + + + 200 + 170 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 175 + 121 + 20 + + + + + 10 + + + + TestRail Project ID + + + + + + 200 + 270 + 231 + 28 + + + + Accept + + + + + false + + + + 270 + 350 + 161 + 22 + + + + + + true + + + + 80 + 350 + 181 + 20 + + + + + 10 + + + + TestRail Added for Release + + + + + + 200 + 20 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 70 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 200 + 215 + 231 + 24 + + + + QLineEdit::Normal + + + + + + 70 + 220 + 121 + 20 + + + + + 10 + + + + TestRail Suite ID + + + + + + urlLineEdit + userLineEdit + passwordLineEdit + projectIDLineEdit + suiteIDLineEdit + acceptButton + releasesComboBox + OKButton + cancelButton + + + + From 8871621372dbded31f5bf027db1b0faa0b455dad Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 18:26:29 -0800 Subject: [PATCH 013/130] Removed duplicated folder. --- tools/nitpick/src/ui/BusyWindow.cpp | 14 - tools/nitpick/src/ui/BusyWindow.h | 22 -- tools/nitpick/src/ui/BusyWindow.ui | 75 ----- tools/nitpick/src/ui/MismatchWindow.cpp | 101 ------- tools/nitpick/src/ui/MismatchWindow.h | 42 --- tools/nitpick/src/ui/MismatchWindow.ui | 199 ------------ .../src/ui/TestRailResultsSelectorWindow.cpp | 106 ------- .../src/ui/TestRailResultsSelectorWindow.h | 50 ---- .../src/ui/TestRailResultsSelectorWindow.ui | 280 ----------------- .../src/ui/TestRailRunSelectorWindow.cpp | 101 ------- .../src/ui/TestRailRunSelectorWindow.h | 50 ---- .../src/ui/TestRailRunSelectorWindow.ui | 283 ------------------ .../ui/TestRailTestCasesSelectorWindow.cpp | 106 ------- .../src/ui/TestRailTestCasesSelectorWindow.h | 50 ---- .../src/ui/TestRailTestCasesSelectorWindow.ui | 280 ----------------- 15 files changed, 1759 deletions(-) delete mode 100644 tools/nitpick/src/ui/BusyWindow.cpp delete mode 100644 tools/nitpick/src/ui/BusyWindow.h delete mode 100644 tools/nitpick/src/ui/BusyWindow.ui delete mode 100644 tools/nitpick/src/ui/MismatchWindow.cpp delete mode 100644 tools/nitpick/src/ui/MismatchWindow.h delete mode 100644 tools/nitpick/src/ui/MismatchWindow.ui delete mode 100644 tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp delete mode 100644 tools/nitpick/src/ui/TestRailResultsSelectorWindow.h delete mode 100644 tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui delete mode 100644 tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp delete mode 100644 tools/nitpick/src/ui/TestRailRunSelectorWindow.h delete mode 100644 tools/nitpick/src/ui/TestRailRunSelectorWindow.ui delete mode 100644 tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp delete mode 100644 tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h delete mode 100644 tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui diff --git a/tools/nitpick/src/ui/BusyWindow.cpp b/tools/nitpick/src/ui/BusyWindow.cpp deleted file mode 100644 index 8066d576f8..0000000000 --- a/tools/nitpick/src/ui/BusyWindow.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// -// BusyWindow.cpp -// -// Created by Nissim Hadar on 26 Jul 2017. -// Copyright 2013 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 "BusyWindow.h" - -BusyWindow::BusyWindow(QWidget *parent) { - setupUi(this); -} diff --git a/tools/nitpick/src/ui/BusyWindow.h b/tools/nitpick/src/ui/BusyWindow.h deleted file mode 100644 index 62f2df7e04..0000000000 --- a/tools/nitpick/src/ui/BusyWindow.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// BusyWindow.h -// -// Created by Nissim Hadar on 29 Jul 2017. -// Copyright 2013 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_BusyWindow_h -#define hifi_BusyWindow_h - -#include "ui_BusyWindow.h" - -class BusyWindow : public QDialog, public Ui::BusyWindow { - Q_OBJECT - -public: - BusyWindow(QWidget* parent = Q_NULLPTR); -}; - -#endif \ No newline at end of file diff --git a/tools/nitpick/src/ui/BusyWindow.ui b/tools/nitpick/src/ui/BusyWindow.ui deleted file mode 100644 index c237566a5e..0000000000 --- a/tools/nitpick/src/ui/BusyWindow.ui +++ /dev/null @@ -1,75 +0,0 @@ - - - BusyWindow - - - Qt::ApplicationModal - - - - 0 - 0 - 542 - 189 - - - - Updating TestRail - please wait - - - - - 30 - 850 - 500 - 28 - - - - - 12 - - - - similarity - - - - - - 40 - 40 - 481 - 101 - - - - 0 - - - 0 - - - - - - 50 - 60 - 431 - 61 - - - - - 20 - - - - Please wait for this window to close - - - - - - - diff --git a/tools/nitpick/src/ui/MismatchWindow.cpp b/tools/nitpick/src/ui/MismatchWindow.cpp deleted file mode 100644 index 58189b4795..0000000000 --- a/tools/nitpick/src/ui/MismatchWindow.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// -// MismatchWindow.cpp -// -// Created by Nissim Hadar on 9 Nov 2017. -// Copyright 2013 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 "MismatchWindow.h" - -#include - -#include - -MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { - setupUi(this); - - expectedImage->setScaledContents(true); - resultImage->setScaledContents(true); - diffImage->setScaledContents(true); -} - -QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { - // Create an empty difference image if the images differ in size - if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { - return QPixmap(); - } - - // This is an optimization, as QImage.setPixel() is embarrassingly slow - unsigned char* buffer = new unsigned char[expectedImage.height() * expectedImage.width() * 3]; - - // loop over each pixel - for (int y = 0; y < expectedImage.height(); ++y) { - for (int x = 0; x < expectedImage.width(); ++x) { - QRgb pixelP = expectedImage.pixel(QPoint(x, y)); - QRgb pixelQ = resultImage.pixel(QPoint(x, y)); - - // Convert to luminance - double p = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); - double q = R_Y * qRed(pixelQ) + G_Y * qGreen(pixelQ) + B_Y * qBlue(pixelQ); - - // The intensity value is modified to increase the brightness of the displayed image - double absoluteDifference = fabs(p - q) / 255.0; - double modifiedDifference = sqrt(absoluteDifference); - - int difference = (int)(modifiedDifference * 255.0); - - buffer[3 * (x + y * expectedImage.width()) + 0] = difference; - buffer[3 * (x + y * expectedImage.width()) + 1] = difference; - buffer[3 * (x + y * expectedImage.width()) + 2] = difference; - } - } - - QImage diffImage(buffer, expectedImage.width(), expectedImage.height(), QImage::Format_RGB888); - QPixmap resultPixmap = QPixmap::fromImage(diffImage); - - delete[] buffer; - - return resultPixmap; -} - -void MismatchWindow::setTestResult(TestResult testResult) { - errorLabel->setText("Similarity: " + QString::number(testResult._error)); - - imagePath->setText("Path to test: " + testResult._pathname); - - expectedFilename->setText(testResult._expectedImageFilename); - resultFilename->setText(testResult._actualImageFilename); - - QPixmap expectedPixmap = QPixmap(testResult._pathname + testResult._expectedImageFilename); - QPixmap actualPixmap = QPixmap(testResult._pathname + testResult._actualImageFilename); - - _diffPixmap = computeDiffPixmap( - QImage(testResult._pathname + testResult._expectedImageFilename), - QImage(testResult._pathname + testResult._actualImageFilename) - ); - - expectedImage->setPixmap(expectedPixmap); - resultImage->setPixmap(actualPixmap); - diffImage->setPixmap(_diffPixmap); -} - -void MismatchWindow::on_passTestButton_clicked() { - _userResponse = USER_RESPONSE_PASS; - close(); -} - -void MismatchWindow::on_failTestButton_clicked() { - _userResponse = USE_RESPONSE_FAIL; - close(); -} - -void MismatchWindow::on_abortTestsButton_clicked() { - _userResponse = USER_RESPONSE_ABORT; - close(); -} - -QPixmap MismatchWindow::getComparisonImage() { - return _diffPixmap; -} diff --git a/tools/nitpick/src/ui/MismatchWindow.h b/tools/nitpick/src/ui/MismatchWindow.h deleted file mode 100644 index 30c29076b3..0000000000 --- a/tools/nitpick/src/ui/MismatchWindow.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// MismatchWindow.h -// -// Created by Nissim Hadar on 9 Nov 2017. -// Copyright 2013 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_MismatchWindow_h -#define hifi_MismatchWindow_h - -#include "ui_MismatchWindow.h" - -#include "../common.h" - -class MismatchWindow : public QDialog, public Ui::MismatchWindow { - Q_OBJECT - -public: - MismatchWindow(QWidget *parent = Q_NULLPTR); - - void setTestResult(TestResult testResult); - - UserResponse getUserResponse() { return _userResponse; } - - QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); - QPixmap getComparisonImage(); - -private slots: - void on_passTestButton_clicked(); - void on_failTestButton_clicked(); - void on_abortTestsButton_clicked(); - -private: - UserResponse _userResponse{ USER_RESPONSE_INVALID }; - - QPixmap _diffPixmap; -}; - - -#endif // hifi_MismatchWindow_h \ No newline at end of file diff --git a/tools/nitpick/src/ui/MismatchWindow.ui b/tools/nitpick/src/ui/MismatchWindow.ui deleted file mode 100644 index 8a174989d4..0000000000 --- a/tools/nitpick/src/ui/MismatchWindow.ui +++ /dev/null @@ -1,199 +0,0 @@ - - - MismatchWindow - - - Qt::ApplicationModal - - - - 0 - 0 - 1782 - 942 - - - - MismatchWindow - - - - - 10 - 25 - 800 - 450 - - - - expected image - - - - - - 900 - 25 - 800 - 450 - - - - result image - - - - - - 540 - 480 - 800 - 450 - - - - diff image - - - - - - 60 - 660 - 480 - 28 - - - - - 12 - - - - result image filename - - - - - - 60 - 630 - 480 - 28 - - - - - 12 - - - - expected image filename - - - - - - 20 - 600 - 1200 - 28 - - - - - 12 - - - - image path - - - - - - 30 - 790 - 75 - 23 - - - - Pass - - - - - - 120 - 790 - 75 - 23 - - - - Fail - - - - - - 210 - 790 - 121 - 23 - - - - Abort current test - - - - - - 30 - 850 - 500 - 28 - - - - - 12 - - - - similarity - - - - - - 30 - 5 - 151 - 16 - - - - Expected Image - - - - - - 930 - 5 - 151 - 16 - - - - Actual Image - - - - - - - diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp deleted file mode 100644 index 505e04b33e..0000000000 --- a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// -// TestRailResultsSelectorWindow.cpp -// -// Created by Nissim Hadar on 2 Aug 2017. -// Copyright 2013 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 "TestRailResultsSelectorWindow.h" - -#include - -#include - -TestRailResultsSelectorWindow::TestRailResultsSelectorWindow(QWidget *parent) { - setupUi(this); - - projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); -} - - -void TestRailResultsSelectorWindow::reset() { - urlLineEdit->setDisabled(false); - userLineEdit->setDisabled(false); - passwordLineEdit->setDisabled(false); - projectIDLineEdit->setDisabled(false); - suiteIDLineEdit->setDisabled(false); - - OKButton->setDisabled(true); - - runsLabel->setDisabled(true); - runsComboBox->setDisabled(true); -} - -void TestRailResultsSelectorWindow::on_acceptButton_clicked() { - urlLineEdit->setDisabled(true); - userLineEdit->setDisabled(true); - passwordLineEdit->setDisabled(true); - projectIDLineEdit->setDisabled(true); - suiteIDLineEdit->setDisabled(true); - - OKButton->setDisabled(false); - - runsLabel->setDisabled(false); - runsComboBox->setDisabled(false); - close(); -} - -void TestRailResultsSelectorWindow::on_OKButton_clicked() { - userCancelled = false; - close(); -} - -void TestRailResultsSelectorWindow::on_cancelButton_clicked() { - userCancelled = true; - close(); -} - -bool TestRailResultsSelectorWindow::getUserCancelled() { - return userCancelled; -} - -void TestRailResultsSelectorWindow::setURL(const QString& user) { - urlLineEdit->setText(user); -} - -QString TestRailResultsSelectorWindow::getURL() { - return urlLineEdit->text(); -} - -void TestRailResultsSelectorWindow::setUser(const QString& user) { - userLineEdit->setText(user); -} - -QString TestRailResultsSelectorWindow::getUser() { - return userLineEdit->text(); -} - -QString TestRailResultsSelectorWindow::getPassword() { - return passwordLineEdit->text(); -} - -void TestRailResultsSelectorWindow::setProjectID(const int project) { - projectIDLineEdit->setText(QString::number(project)); -} - -int TestRailResultsSelectorWindow::getProjectID() { - return projectIDLineEdit->text().toInt(); -} - -void TestRailResultsSelectorWindow::setSuiteID(const int project) { - suiteIDLineEdit->setText(QString::number(project)); -} - -int TestRailResultsSelectorWindow::getSuiteID() { - return suiteIDLineEdit->text().toInt(); -} - -void TestRailResultsSelectorWindow::updateRunsComboBoxData(QStringList data) { - runsComboBox->insertItems(0, data); -} - -int TestRailResultsSelectorWindow::getRunID() { - return runsComboBox->currentIndex(); -} \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.h b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.h deleted file mode 100644 index 51059d6127..0000000000 --- a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// TestRailResultsSelectorWindow.h -// -// Created by Nissim Hadar on 2 Aug 2017. -// Copyright 2013 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_TestRailResultsSelectorWindow_h -#define hifi_TestRailResultsSelectorWindow_h - -#include "ui_TestRailResultsSelectorWindow.h" - -class TestRailResultsSelectorWindow : public QDialog, public Ui::TestRailResultsSelectorWindow { - Q_OBJECT - -public: - TestRailResultsSelectorWindow(QWidget* parent = Q_NULLPTR); - - void reset(); - - bool getUserCancelled(); - - void setURL(const QString& user); - QString getURL(); - - void setUser(const QString& user); - QString getUser(); - - QString getPassword(); - - void setProjectID(const int project); - int getProjectID(); - - void setSuiteID(const int project); - int getSuiteID(); - - bool userCancelled{ false }; - - void updateRunsComboBoxData(QStringList data); - int getRunID(); - -private slots: - void on_acceptButton_clicked(); - void on_OKButton_clicked(); - void on_cancelButton_clicked(); -}; - -#endif \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui b/tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui deleted file mode 100644 index 983b95ee79..0000000000 --- a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui +++ /dev/null @@ -1,280 +0,0 @@ - - - TestRailResultsSelectorWindow - - - - 0 - 0 - 533 - 474 - - - - TestRail Test Case Selector Window - - - - - 30 - 850 - 500 - 28 - - - - - 12 - - - - similarity - - - - - - 70 - 125 - 121 - 20 - - - - - 10 - - - - TestRail Password - - - - - - 70 - 25 - 121 - 20 - - - - - 10 - - - - TestRail URL - - - - - false - - - - 120 - 420 - 93 - 28 - - - - OK - - - - - - 280 - 420 - 93 - 28 - - - - Cancel - - - - - - 200 - 120 - 231 - 24 - - - - QLineEdit::Password - - - - - - 70 - 75 - 121 - 20 - - - - - 10 - - - - TestRail User - - - - - - 200 - 170 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 175 - 121 - 20 - - - - - 10 - - - - TestRail Project ID - - - - - - 200 - 270 - 231 - 28 - - - - Accept - - - - - false - - - - 160 - 350 - 271 - 22 - - - - - - true - - - - 80 - 350 - 71 - 20 - - - - - 10 - - - - TestRail Run - - - - - - 200 - 20 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 70 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 215 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 220 - 121 - 20 - - - - - 10 - - - - TestRail Suite ID - - - - - - urlLineEdit - userLineEdit - passwordLineEdit - projectIDLineEdit - suiteIDLineEdit - acceptButton - runsComboBox - OKButton - cancelButton - - - - diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp deleted file mode 100644 index ac3419d46f..0000000000 --- a/tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// -// TestRailRunSelectorWindow.cpp -// -// Created by Nissim Hadar on 31 Jul 2017. -// Copyright 2013 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 "TestRailRunSelectorWindow.h" - -#include - -#include - -TestRailRunSelectorWindow::TestRailRunSelectorWindow(QWidget *parent) { - setupUi(this); - - projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); -} - -void TestRailRunSelectorWindow::reset() { - urlLineEdit->setDisabled(false); - userLineEdit->setDisabled(false); - passwordLineEdit->setDisabled(false); - projectIDLineEdit->setDisabled(false); - suiteIDLineEdit->setDisabled(false); - - OKButton->setDisabled(true); - sectionsComboBox->setDisabled(true); -} - -void TestRailRunSelectorWindow::on_acceptButton_clicked() { - urlLineEdit->setDisabled(true); - userLineEdit->setDisabled(true); - passwordLineEdit->setDisabled(true); - projectIDLineEdit->setDisabled(true); - suiteIDLineEdit->setDisabled(true); - - OKButton->setDisabled(false); - sectionsComboBox->setDisabled(false); - close(); -} - -void TestRailRunSelectorWindow::on_OKButton_clicked() { - userCancelled = false; - close(); -} - -void TestRailRunSelectorWindow::on_cancelButton_clicked() { - userCancelled = true; - close(); -} - -bool TestRailRunSelectorWindow::getUserCancelled() { - return userCancelled; -} - -void TestRailRunSelectorWindow::setURL(const QString& user) { - urlLineEdit->setText(user); -} - -QString TestRailRunSelectorWindow::getURL() { - return urlLineEdit->text(); -} - -void TestRailRunSelectorWindow::setUser(const QString& user) { - userLineEdit->setText(user); -} - -QString TestRailRunSelectorWindow::getUser() { - return userLineEdit->text(); -} - -QString TestRailRunSelectorWindow::getPassword() { - return passwordLineEdit->text(); -} - -void TestRailRunSelectorWindow::setProjectID(const int project) { - projectIDLineEdit->setText(QString::number(project)); -} - -int TestRailRunSelectorWindow::getProjectID() { - return projectIDLineEdit->text().toInt(); -} - -void TestRailRunSelectorWindow::setSuiteID(const int project) { - suiteIDLineEdit->setText(QString::number(project)); -} - -int TestRailRunSelectorWindow::getSuiteID() { - return suiteIDLineEdit->text().toInt(); -} - -void TestRailRunSelectorWindow::updateSectionsComboBoxData(QStringList data) { - sectionsComboBox->insertItems(0, data); -} - -int TestRailRunSelectorWindow::getSectionID() { - return sectionsComboBox->currentIndex(); -} \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.h b/tools/nitpick/src/ui/TestRailRunSelectorWindow.h deleted file mode 100644 index d6428bb476..0000000000 --- a/tools/nitpick/src/ui/TestRailRunSelectorWindow.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// TestRailRunSelectorWindow.h -// -// Created by Nissim Hadar on 31 Jul 2017. -// Copyright 2013 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_TestRailRunSelectorWindow_h -#define hifi_TestRailRunSelectorWindow_h - -#include "ui_TestRailRunSelectorWindow.h" - -class TestRailRunSelectorWindow : public QDialog, public Ui::TestRailRunSelectorWindow { - Q_OBJECT - -public: - TestRailRunSelectorWindow(QWidget* parent = Q_NULLPTR); - - void reset(); - - bool getUserCancelled(); - - void setURL(const QString& user); - QString getURL(); - - void setUser(const QString& user); - QString getUser(); - - QString getPassword(); - - void setProjectID(const int project); - int getProjectID(); - - void setSuiteID(const int project); - int getSuiteID(); - - bool userCancelled{ false }; - - void updateSectionsComboBoxData(QStringList data); - int getSectionID(); - -private slots: - void on_acceptButton_clicked(); - void on_OKButton_clicked(); - void on_cancelButton_clicked(); -}; - -#endif \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.ui b/tools/nitpick/src/ui/TestRailRunSelectorWindow.ui deleted file mode 100644 index ad39b5cc64..0000000000 --- a/tools/nitpick/src/ui/TestRailRunSelectorWindow.ui +++ /dev/null @@ -1,283 +0,0 @@ - - - TestRailRunSelectorWindow - - - Qt::ApplicationModal - - - - 0 - 0 - 489 - 474 - - - - TestRail Run Selector Window - - - - - 30 - 850 - 500 - 28 - - - - - 12 - - - - similarity - - - - - - 70 - 125 - 121 - 20 - - - - - 10 - - - - TestRail Password - - - - - - 70 - 25 - 121 - 20 - - - - - 10 - - - - TestRail URL - - - - - false - - - - 120 - 420 - 93 - 28 - - - - OK - - - - - - 280 - 420 - 93 - 28 - - - - Cancel - - - - - - 200 - 120 - 231 - 24 - - - - QLineEdit::Password - - - - - - 70 - 75 - 121 - 20 - - - - - 10 - - - - TestRail User - - - - - - 200 - 170 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 175 - 121 - 20 - - - - - 10 - - - - TestRail Project ID - - - - - - 200 - 270 - 231 - 28 - - - - Accept - - - - - false - - - - 140 - 350 - 311 - 22 - - - - - - true - - - - 20 - 350 - 121 - 20 - - - - - 10 - - - - TestRail Sections - - - - - - 200 - 20 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 70 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 215 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 220 - 121 - 20 - - - - - 10 - - - - TestRail Suite ID - - - - - - urlLineEdit - userLineEdit - passwordLineEdit - projectIDLineEdit - suiteIDLineEdit - acceptButton - sectionsComboBox - OKButton - cancelButton - - - - diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp deleted file mode 100644 index 638fe71819..0000000000 --- a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// -// TestRailTestCasesSelectorWindow.cpp -// -// Created by Nissim Hadar on 26 Jul 2017. -// Copyright 2013 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 "TestRailTestCasesSelectorWindow.h" - -#include - -#include - -TestRailTestCasesSelectorWindow::TestRailTestCasesSelectorWindow(QWidget *parent) { - setupUi(this); - - projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); -} - - -void TestRailTestCasesSelectorWindow::reset() { - urlLineEdit->setDisabled(false); - userLineEdit->setDisabled(false); - passwordLineEdit->setDisabled(false); - projectIDLineEdit->setDisabled(false); - suiteIDLineEdit->setDisabled(false); - - OKButton->setDisabled(true); - - releasesLabel->setDisabled(true); - releasesComboBox->setDisabled(true); -} - -void TestRailTestCasesSelectorWindow::on_acceptButton_clicked() { - urlLineEdit->setDisabled(true); - userLineEdit->setDisabled(true); - passwordLineEdit->setDisabled(true); - projectIDLineEdit->setDisabled(true); - suiteIDLineEdit->setDisabled(true); - - OKButton->setDisabled(false); - - releasesLabel->setDisabled(false); - releasesComboBox->setDisabled(false); - close(); -} - -void TestRailTestCasesSelectorWindow::on_OKButton_clicked() { - userCancelled = false; - close(); -} - -void TestRailTestCasesSelectorWindow::on_cancelButton_clicked() { - userCancelled = true; - close(); -} - -bool TestRailTestCasesSelectorWindow::getUserCancelled() { - return userCancelled; -} - -void TestRailTestCasesSelectorWindow::setURL(const QString& user) { - urlLineEdit->setText(user); -} - -QString TestRailTestCasesSelectorWindow::getURL() { - return urlLineEdit->text(); -} - -void TestRailTestCasesSelectorWindow::setUser(const QString& user) { - userLineEdit->setText(user); -} - -QString TestRailTestCasesSelectorWindow::getUser() { - return userLineEdit->text(); -} - -QString TestRailTestCasesSelectorWindow::getPassword() { - return passwordLineEdit->text(); -} - -void TestRailTestCasesSelectorWindow::setProjectID(const int project) { - projectIDLineEdit->setText(QString::number(project)); -} - -int TestRailTestCasesSelectorWindow::getProjectID() { - return projectIDLineEdit->text().toInt(); -} - -void TestRailTestCasesSelectorWindow::setSuiteID(const int project) { - suiteIDLineEdit->setText(QString::number(project)); -} - -int TestRailTestCasesSelectorWindow::getSuiteID() { - return suiteIDLineEdit->text().toInt(); -} - -void TestRailTestCasesSelectorWindow::updateReleasesComboBoxData(QStringList data) { - releasesComboBox->insertItems(0, data); -} - -int TestRailTestCasesSelectorWindow::getReleaseID() { - return releasesComboBox->currentIndex(); -} \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h deleted file mode 100644 index 9153b003fa..0000000000 --- a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// TestRailTestCasesSelectorWindow.h -// -// Created by Nissim Hadar on 26 Jul 2017. -// Copyright 2013 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_TestRailTestCasesSelectorWindow_h -#define hifi_TestRailTestCasesSelectorWindow_h - -#include "ui_TestRailTestCasesSelectorWindow.h" - -class TestRailTestCasesSelectorWindow : public QDialog, public Ui::TestRailTestCasesSelectorWindow { - Q_OBJECT - -public: - TestRailTestCasesSelectorWindow(QWidget* parent = Q_NULLPTR); - - void reset(); - - bool getUserCancelled(); - - void setURL(const QString& user); - QString getURL(); - - void setUser(const QString& user); - QString getUser(); - - QString getPassword(); - - void setProjectID(const int project); - int getProjectID(); - - void setSuiteID(const int project); - int getSuiteID(); - - bool userCancelled{ false }; - - void updateReleasesComboBoxData(QStringList data); - int getReleaseID(); - -private slots: - void on_acceptButton_clicked(); - void on_OKButton_clicked(); - void on_cancelButton_clicked(); -}; - -#endif \ No newline at end of file diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui b/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui deleted file mode 100644 index 41ff2943d5..0000000000 --- a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui +++ /dev/null @@ -1,280 +0,0 @@ - - - TestRailTestCasesSelectorWindow - - - - 0 - 0 - 489 - 474 - - - - TestRail Test Case Selector Window - - - - - 30 - 850 - 500 - 28 - - - - - 12 - - - - similarity - - - - - - 70 - 125 - 121 - 20 - - - - - 10 - - - - TestRail Password - - - - - - 70 - 25 - 121 - 20 - - - - - 10 - - - - TestRail URL - - - - - false - - - - 120 - 420 - 93 - 28 - - - - OK - - - - - - 280 - 420 - 93 - 28 - - - - Cancel - - - - - - 200 - 120 - 231 - 24 - - - - QLineEdit::Password - - - - - - 70 - 75 - 121 - 20 - - - - - 10 - - - - TestRail User - - - - - - 200 - 170 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 175 - 121 - 20 - - - - - 10 - - - - TestRail Project ID - - - - - - 200 - 270 - 231 - 28 - - - - Accept - - - - - false - - - - 270 - 350 - 161 - 22 - - - - - - true - - - - 80 - 350 - 181 - 20 - - - - - 10 - - - - TestRail Added for Release - - - - - - 200 - 20 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 70 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 200 - 215 - 231 - 24 - - - - QLineEdit::Normal - - - - - - 70 - 220 - 121 - 20 - - - - - 10 - - - - TestRail Suite ID - - - - - - urlLineEdit - userLineEdit - passwordLineEdit - projectIDLineEdit - suiteIDLineEdit - acceptButton - releasesComboBox - OKButton - cancelButton - - - - From 7a957d829ce7a92d9c525d4912e5c63c5c62c666 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 19:36:06 -0800 Subject: [PATCH 014/130] Missing files. --- tools/nitpick/compiledResources/resources.rcc | Bin 0 -> 42 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/nitpick/compiledResources/resources.rcc diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc new file mode 100644 index 0000000000000000000000000000000000000000..15f51ed7f4d9aa2328eca21473fd352cf3605021 GIT binary patch literal 42 dcmXRcN-bt!U|?ckU=TsV5D^ey1d|L53;<0C0sQ~~ literal 0 HcmV?d00001 From e9ffa05b4b27087046337662eec75e0f86b190d5 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 20:27:23 -0800 Subject: [PATCH 015/130] Eliminate silly gcc warnings. --- tools/nitpick/src/TestRunnerMobile.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 480381dd8e..782295ae03 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -79,6 +79,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { } void TestRunnerMobile::connectDevice() { +#if defined Q_OS_WIN or defined Q_OS_MAC QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbCommand + " devices > " + devicesFullFilename; system(command.toStdString().c_str()); @@ -108,6 +109,7 @@ void TestRunnerMobile::connectDevice() { _downloadAPKPushbutton->setEnabled(true); } } +#endif } void TestRunnerMobile::downloadAPK() { @@ -152,15 +154,19 @@ void TestRunnerMobile::downloadComplete() { } void TestRunnerMobile::installAPK() { +#if defined Q_OS_WIN or defined Q_OS_MAC _statusLabel->setText("Installing"); QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); +#endif } void TestRunnerMobile::pullFolder() { +#if defined Q_OS_WIN or defined Q_OS_MAC _statusLabel->setText("Pulling folder"); QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; system(command.toStdString().c_str()); _statusLabel->setText("Pull complete"); +#endif } From 8223e046c83d2f8771b4767a778ac7ff88d311ea Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 25 Jan 2019 21:41:00 -0800 Subject: [PATCH 016/130] Eliminate silly gcc warnings. --- tools/nitpick/src/TestRunnerMobile.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 782295ae03..6bc34f26a8 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -79,7 +79,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { } void TestRunnerMobile::connectDevice() { -#if defined Q_OS_WIN or defined Q_OS_MAC +#if defined Q_OS_WIN || defined Q_OS_MAC QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbCommand + " devices > " + devicesFullFilename; system(command.toStdString().c_str()); @@ -154,7 +154,7 @@ void TestRunnerMobile::downloadComplete() { } void TestRunnerMobile::installAPK() { -#if defined Q_OS_WIN or defined Q_OS_MAC +#if defined Q_OS_WIN || defined Q_OS_MAC _statusLabel->setText("Installing"); QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; system(command.toStdString().c_str()); @@ -163,7 +163,7 @@ void TestRunnerMobile::installAPK() { } void TestRunnerMobile::pullFolder() { -#if defined Q_OS_WIN or defined Q_OS_MAC +#if defined Q_OS_WIN || defined Q_OS_MAC _statusLabel->setText("Pulling folder"); QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; system(command.toStdString().c_str()); From bee86235a33450405445605d15fe11bd86877fd3 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 26 Jan 2019 09:52:58 -0800 Subject: [PATCH 017/130] Corrected error message. --- tools/nitpick/src/TestRunnerMobile.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 6bc34f26a8..e7191dcfad 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -64,8 +64,8 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { _adbCommand = adbExePath + "/" + _adbExe; } else { - QMessageBox::critical(0, "PYTHON_PATH not defined", - "Please set PYTHON_PATH to directory containing the Python executable"); + QMessageBox::critical(0, "ADB_PATH not defined", + "Please set ADB_PATH to directory containing the `adb` executable"); exit(-1); } #elif defined Q_OS_MAC From 94d741480c1751b880a7b2877c0498cea177b8e1 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 26 Jan 2019 14:59:30 -0800 Subject: [PATCH 018/130] Added missing SSL dll's --- tools/nitpick/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index 9d5d38ec1f..f825775879 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -157,6 +157,13 @@ if (WIN32) COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity" ) endif () + + # add a custom command to copy the SSL DLLs + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" + ) elseif (APPLE) add_custom_command( TARGET ${TARGET_NAME} From f248beba9fd967c44bb52e06c392a17c72dcd84d Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sun, 27 Jan 2019 23:43:07 -0800 Subject: [PATCH 019/130] Added device description + running Interface. --- tools/nitpick/src/Nitpick.cpp | 5 +++ tools/nitpick/src/Nitpick.h | 1 + tools/nitpick/src/TestRunnerMobile.cpp | 46 ++++++++++++++++++++------ tools/nitpick/src/TestRunnerMobile.h | 5 +++ tools/nitpick/ui/Nitpick.ui | 22 ++++++++++-- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 9e385bcd4d..78ed0ca0af 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -113,6 +113,7 @@ void Nitpick::setup() { _ui.folderLineEdit, _ui.downloadAPKPushbutton, _ui.installAPKPushbutton, + _ui.runInterfacePushbutton, _ui.runLatestOnMobileCheckBox, _ui.urlOnMobileLineEdit, _ui.statusLabelOnMobile @@ -367,6 +368,10 @@ void Nitpick::on_installAPKPushbutton_clicked() { _testRunnerMobile->installAPK(); } +void Nitpick::on_runInterfacePushbutton_clicked() { + _testRunnerMobile->runInterface(); +} + void Nitpick::on_pullFolderPushbutton_clicked() { _testRunnerMobile->pullFolder(); } diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 00516d1e76..29726be3bd 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -101,6 +101,7 @@ private slots: void on_downloadAPKPushbutton_clicked(); void on_installAPKPushbutton_clicked(); + void on_runInterfacePushbutton_clicked(); void on_pullFolderPushbutton_clicked(); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index e7191dcfad..10216f248c 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -24,6 +24,7 @@ TestRunnerMobile::TestRunnerMobile( QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, QPushButton* installAPKPushbutton, + QPushButton* runInterfacePushbutton, QCheckBox* runLatest, QLineEdit* url, QLabel* statusLabel, @@ -38,22 +39,16 @@ TestRunnerMobile::TestRunnerMobile( _folderLineEdit = folderLineEdit; _downloadAPKPushbutton = downloadAPKPushbutton; _installAPKPushbutton = installAPKPushbutton; + _runInterfacePushbutton = runInterfacePushbutton; _runLatest = runLatest; _url = url; _statusLabel = statusLabel; folderLineEdit->setText("/sdcard/DCIM/TEST"); -} -TestRunnerMobile::~TestRunnerMobile() { -} + modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; -void TestRunnerMobile::setWorkingFolderAndEnableControls() { - setWorkingFolder(_workingFolderLabel); - - _connectDeviceButton->setEnabled(true); - - // Find ADB (Android Debugging Bridge) before continuing + // Find ADB (Android Debugging Bridge) #ifdef Q_OS_WIN if (QProcessEnvironment::systemEnvironment().contains("ADB_PATH")) { QString adbExePath = QProcessEnvironment::systemEnvironment().value("ADB_PATH") + "/platform-tools"; @@ -78,10 +73,19 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { #endif } +TestRunnerMobile::~TestRunnerMobile() { +} + +void TestRunnerMobile::setWorkingFolderAndEnableControls() { + setWorkingFolder(_workingFolderLabel); + + _connectDeviceButton->setEnabled(true); +} + void TestRunnerMobile::connectDevice() { #if defined Q_OS_WIN || defined Q_OS_MAC QString devicesFullFilename{ _workingFolder + "/devices.txt" }; - QString command = _adbCommand + " devices > " + devicesFullFilename; + QString command = _adbCommand + " devices -l > " + devicesFullFilename; system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { @@ -103,7 +107,17 @@ void TestRunnerMobile::connectDevice() { QMessageBox::critical(0, "Too many devices detected", "Tests will run only if a single device is attached"); } else { - _detectedDeviceLabel->setText(line2.remove(DEVICE)); + // Line looks like this: 988a1b47335239434b device product:dream2qlteue model:SM_G955U1 device:dream2qlteue transport_id:2 + QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); + QString deviceID = tokens[0]; + + QString modelID = tokens[3].split(':')[1]; + QString modelName = "UKNOWN"; + if (modelNames.count(modelID) == 1) { + modelName = modelNames[modelID]; + } + + _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); @@ -159,6 +173,16 @@ void TestRunnerMobile::installAPK() { QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); + _runInterfacePushbutton->setEnabled(true); +#endif +} + +void TestRunnerMobile::runInterface() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Starting Interface"); + QString command = _adbCommand + " shell monkey -p io.highfidelity.hifiinterface -v 1"; + system(command.toStdString().c_str()); + _statusLabel->setText("Interface started"); #endif } diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 4cf31f6bd4..247f864976 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -11,6 +11,7 @@ #ifndef hifi_testRunnerMobile_h #define hifi_testRunnerMobile_h +#include #include #include #include @@ -28,6 +29,7 @@ public: QLineEdit *folderLineEdit, QPushButton* downloadAPKPushbutton, QPushButton* installAPKPushbutton, + QPushButton* runInterfacePushbutton, QCheckBox* runLatest, QLineEdit* url, QLabel* statusLabel, @@ -41,6 +43,7 @@ public: void downloadComplete(); void downloadAPK(); + void runInterface(); void installAPK(); @@ -53,6 +56,7 @@ private: QLineEdit* _folderLineEdit; QPushButton* _downloadAPKPushbutton; QPushButton* _installAPKPushbutton; + QPushButton* _runInterfacePushbutton; #ifdef Q_OS_WIN const QString _adbExe{ "adb.exe" }; @@ -65,5 +69,6 @@ private: QString _adbCommand; + std::map modelNames; }; #endif diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 8d69317369..319452233f 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -43,7 +43,7 @@ - 0 + 3 @@ -613,7 +613,7 @@ 460 - 350 + 410 160 30 @@ -629,7 +629,7 @@ 10 - 350 + 410 440 30 @@ -725,6 +725,22 @@ Install APK + + + false + + + + 10 + 300 + 160 + 30 + + + + Run Interface + + From e112bf19cba9c09609b7533092c6ad268f5d5873 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Mon, 28 Jan 2019 11:08:09 -0800 Subject: [PATCH 020/130] gltf color attribute --- libraries/fbx/src/GLTFSerializer.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) mode change 100644 => 100755 libraries/fbx/src/GLTFSerializer.cpp diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp old mode 100644 new mode 100755 index 96c236f703..c2fdc4f0bd --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -831,6 +831,27 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { for (int n = 0; n < normals.size(); n = n + 3) { mesh.normals.push_back(glm::vec3(normals[n], normals[n + 1], normals[n + 2])); } + } else if (key == "COLOR_0") { + QVector colors; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + colors, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF COLOR_0 data for model " << _url; + continue; + } + if (accessor.type == 3) { + for (int n = 0; n < colors.size(); n = n + 4) { + mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); + } + } else { + for (int n = 0; n < colors.size(); n = n + 3) { + mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); + } + } } else if (key == "TEXCOORD_0") { QVector texcoords; success = addArrayOfType(buffer.blob, @@ -926,7 +947,6 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas //_file.dump(); auto hfmModelPtr = std::make_shared(); HFMModel& hfmModel = *hfmModelPtr; - buildGeometry(hfmModel, _url); //hfmDebugDump(data); From 8357f1f9b5cbc792ff0ad45cbb4de752452e0f4b Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 28 Jan 2019 11:10:35 -0800 Subject: [PATCH 021/130] CR fixes --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 4 +++- .../qml/hifi/commerce/marketplace/MarketplaceListItem.qml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 10b59ac83b..cbeedfbb79 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -728,7 +728,7 @@ Rectangle { top: parent.top leftMargin: 20 } - width: 322 + width: root.isLoggedIn ? 322 : 242 height: 36 radius: 4 @@ -790,6 +790,8 @@ Rectangle { glyph: model.glyph text: model.name + visible: root.isLoggedIn || model.sortString != "my_likes" + checked: ListView.isCurrentItem onClicked: { diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml index eb99106cf4..2f37637e40 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -185,7 +185,7 @@ Rectangle { bottom: parent.bottom } onClicked: { - if(isLoggedIn) { + if (isLoggedIn) { root.liked = !root.liked; root.likes = root.liked ? root.likes + 1 : root.likes - 1; MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); @@ -203,7 +203,7 @@ Rectangle { left: parent.left right: parent.right } - height: width*0.5625 + height: width * 0.5625 source: root.image_url fillMode: Image.PreserveAspectCrop From 2fe78383edc9c2f268b0a1ebaf9c4f1a97034257 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 28 Jan 2019 15:14:49 -0800 Subject: [PATCH 022/130] QmlMarketplace - Fix issues found during test * command line --url hifiapp:MARKET was not bringing up correct marketplace * Search field was not being cleared when 'home' button was clicked after a search * tablet button was not lit when marketplace was launched --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 1 + interface/src/commerce/QmlCommerce.cpp | 3 +-- scripts/system/marketplaces/marketplaces.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index cbeedfbb79..8ba5d0bac0 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -665,6 +665,7 @@ Rectangle { categoriesListView.currentIndex = -1; categoriesText.text = "Categories"; root.categoryString = ""; + searchField.text = ""; getMarketplaceItems(); } } diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 5236c5a7fb..046e697b6d 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -54,11 +54,10 @@ void QmlCommerce::openSystemApp(const QString& appName) { {"GOTO", "hifi/tablet/TabletAddressDialog.qml"}, {"PEOPLE", "hifi/Pal.qml"}, {"WALLET", "hifi/commerce/wallet/Wallet.qml"}, - {"MARKET", "/marketplace.html"} + {"MARKET", "hifi/commerce/marketplace/Marketplace.qml"} }; static const QMap systemInject{ - {"MARKET", "/scripts/system/html/js/marketplacesInject.js"} }; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 655d286049..9d571d9284 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -703,7 +703,7 @@ var onMarketplaceScreen = false; var onWalletScreen = false; var onTabletScreenChanged = function onTabletScreenChanged(type, url) { ui.setCurrentVisibleScreenMetadata(type, url); - onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; + onMarketplaceScreen = type === "QML" && url.indexOf(MARKETPLACE_QML_PATH) !== -1; onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; var onCommerceScreenNow = type === "QML" && ( From 1d93b6ada0fa63f4c40ee8bec98125b0ec0187e5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 17 Jan 2019 17:51:12 -0800 Subject: [PATCH 023/130] Support gpu timing on systems with disjoint timer extension --- .../src/gpu/gl/GLBackendQuery.cpp | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendQuery.cpp index 61423bf970..7f61ca78f6 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendQuery.cpp @@ -24,6 +24,17 @@ const uint32_t MAX_RANGE_QUERY_DEPTH = 10000; static bool timeElapsed = false; #endif +#if defined(USE_GLES) +static bool hasTimerExtension() { + static std::once_flag once; + static bool result = false; + std::call_once(once, [&] { + result = glGetQueryObjectui64vEXT != nullptr; + }); + return result; +} +#endif + void GLBackend::do_beginQuery(const Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); @@ -33,7 +44,11 @@ void GLBackend::do_beginQuery(const Batch& batch, size_t paramOffset) { ++_queryStage._rangeQueryDepth; glquery->_batchElapsedTimeBegin = std::chrono::high_resolution_clock::now(); -#if !defined(USE_GLES) +#if defined(USE_GLES) + if (hasTimerExtension()) { + glQueryCounterEXT(glquery->_beginqo, GL_TIMESTAMP_EXT); + } +#else if (timeElapsed) { if (_queryStage._rangeQueryDepth <= MAX_RANGE_QUERY_DEPTH) { glBeginQuery(GL_TIME_ELAPSED, glquery->_endqo); @@ -52,7 +67,11 @@ void GLBackend::do_endQuery(const Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { -#if !defined(USE_GLES) +#if defined(USE_GLES) + if (hasTimerExtension()) { + glQueryCounterEXT(glquery->_endqo, GL_TIMESTAMP_EXT); + } +#else if (timeElapsed) { if (_queryStage._rangeQueryDepth <= MAX_RANGE_QUERY_DEPTH) { glEndQuery(GL_TIME_ELAPSED); @@ -79,7 +98,21 @@ void GLBackend::do_getQuery(const Batch& batch, size_t paramOffset) { if (glquery->_rangeQueryDepth > MAX_RANGE_QUERY_DEPTH) { query->triggerReturnHandler(glquery->_result, glquery->_batchElapsedTime); } else { -#if !defined(USE_GLES) +#if defined(USE_GLES) + glquery->_result = 0; + if (hasTimerExtension()) { + glGetQueryObjectui64vEXT(glquery->_endqo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); + if (glquery->_result == GL_TRUE) { + GLuint64 start, end; + glGetQueryObjectui64vEXT(glquery->_beginqo, GL_QUERY_RESULT, &start); + glGetQueryObjectui64vEXT(glquery->_endqo, GL_QUERY_RESULT, &end); + glquery->_result = end - start; + query->triggerReturnHandler(glquery->_result, glquery->_batchElapsedTime); + } + } else { + query->triggerReturnHandler(0, glquery->_batchElapsedTime); + } +#else glGetQueryObjectui64v(glquery->_endqo, GL_QUERY_RESULT_AVAILABLE, &glquery->_result); if (glquery->_result == GL_TRUE) { if (timeElapsed) { @@ -92,9 +125,6 @@ void GLBackend::do_getQuery(const Batch& batch, size_t paramOffset) { } query->triggerReturnHandler(glquery->_result, glquery->_batchElapsedTime); } -#else - // gles3 is not supporting true time query returns just the batch elapsed time - query->triggerReturnHandler(0, glquery->_batchElapsedTime); #endif (void)CHECK_GL_ERROR(); } From ff9280a496ed98d8f0407733d21eaeced4f60dbd Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 25 Jan 2019 17:06:08 -0800 Subject: [PATCH 024/130] Remove unused properties related to mapping from FBXSerializer --- libraries/fbx/src/FBXSerializer.cpp | 12 ++++-------- libraries/fbx/src/GLTFSerializer.cpp | 2 -- libraries/hfm/src/hfm/HFM.h | 3 --- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 38465d2809..25e0c0fd3a 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -444,8 +444,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr std::map lights; - QVariantHash joints = mapping.value("joint").toHash(); - QVariantHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash blendshapeIndices; @@ -473,8 +471,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr HFMModel& hfmModel = *hfmModelPtr; hfmModel.originalURL = url; - hfmModel.hfmToHifiJointNameMapping.clear(); - hfmModel.hfmToHifiJointNameMapping = getJointNameMapping(mapping); + auto hfmToHifiJointNameMapping = getJointNameMapping(mapping); float unitScaleFactor = 1.0f; glm::vec3 ambientColor; @@ -1341,8 +1338,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = fbxModel.name; - if (hfmModel.hfmToHifiJointNameMapping.contains(hfmModel.hfmToHifiJointNameMapping.key(joint.name))) { - joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name); + if (hfmToHifiJointNameMapping.contains(hfmToHifiJointNameMapping.key(joint.name))) { + joint.name = hfmToHifiJointNameMapping.key(fbxModel.name); } joint.bindTransformFoundInCluster = false; @@ -1704,7 +1701,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines); } } - hfmModel.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); @@ -1728,7 +1724,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString jointName = itr.key(); glm::quat rotationOffset = itr.value(); int jointIndex = hfmModel.getJointIndex(jointName); - if (hfmModel.hfmToHifiJointNameMapping.contains(jointName)) { + if (hfmToHifiJointNameMapping.contains(jointName)) { jointIndex = hfmModel.getJointIndex(jointName); } if (jointIndex != -1) { diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 96c236f703..238e7d7fdb 100644 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -1187,8 +1187,6 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " hasSkeletonJoints =" << hfmModel.hasSkeletonJoints; qCDebug(modelformat) << " offset =" << hfmModel.offset; - qCDebug(modelformat) << " palmDirection = " << hfmModel.palmDirection; - qCDebug(modelformat) << " neckPivot = " << hfmModel.neckPivot; qCDebug(modelformat) << " bindExtents.size() = " << hfmModel.bindExtents.size(); diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 1bd87332a1..07528f3348 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -291,8 +291,6 @@ public: glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file - glm::vec3 palmDirection; - glm::vec3 neckPivot; Extents bindExtents; @@ -319,7 +317,6 @@ public: QList blendshapeChannelNames; QMap jointRotationOffsets; - QMap hfmToHifiJointNameMapping; }; }; From 3e7a80ac4c779d367d75f56b78fdbbbcf61191cd Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 25 Jan 2019 18:07:17 -0800 Subject: [PATCH 025/130] Move FST joint name and rig processing to the model preparation step --- libraries/fbx/src/FBXSerializer.cpp | 59 ------------ .../model-baker/src/model-baker/Baker.cpp | 31 +++++-- libraries/model-baker/src/model-baker/Baker.h | 4 +- .../src/model-baker/PrepareJointsTask.cpp | 89 +++++++++++++++++++ .../src/model-baker/PrepareJointsTask.h | 30 +++++++ .../src/model-networking/ModelCache.cpp | 8 +- 6 files changed, 150 insertions(+), 71 deletions(-) create mode 100644 libraries/model-baker/src/model-baker/PrepareJointsTask.cpp create mode 100644 libraries/model-baker/src/model-baker/PrepareJointsTask.h diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 25e0c0fd3a..92d5e7e774 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -384,43 +384,6 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -QMap getJointNameMapping(const QVariantHash& mapping) { - static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; - QMap hfmToHifiJointNameMap; - if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { - auto jointNames = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); - for (auto itr = jointNames.begin(); itr != jointNames.end(); itr++) { - hfmToHifiJointNameMap.insert(itr.key(), itr.value().toString()); - qCDebug(modelformat) << "the mapped key " << itr.key() << " has a value of " << hfmToHifiJointNameMap[itr.key()]; - } - } - return hfmToHifiJointNameMap; -} - -QMap getJointRotationOffsets(const QVariantHash& mapping) { - QMap jointRotationOffsets; - static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; - if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { - auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - QString line = itr.value().toString(); - auto quatCoords = line.split(','); - if (quatCoords.size() == 4) { - float quatX = quatCoords[0].mid(1).toFloat(); - float quatY = quatCoords[1].toFloat(); - float quatZ = quatCoords[2].toFloat(); - float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat(); - if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) { - glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ); - jointRotationOffsets.insert(jointName, rotationOffset); - } - } - } - } - return jointRotationOffsets; -} - HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; @@ -471,7 +434,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr HFMModel& hfmModel = *hfmModelPtr; hfmModel.originalURL = url; - auto hfmToHifiJointNameMapping = getJointNameMapping(mapping); float unitScaleFactor = 1.0f; glm::vec3 ambientColor; @@ -1284,13 +1246,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } // convert the models to joints - QVariantList freeJoints = mapping.values("freeJoint"); hfmModel.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; - joint.isFree = freeJoints.contains(fbxModel.name); joint.parentIndex = fbxModel.parentIndex; // get the indices of all ancestors starting with the first free one (if any) @@ -1338,14 +1298,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = fbxModel.name; - if (hfmToHifiJointNameMapping.contains(hfmToHifiJointNameMapping.key(joint.name))) { - joint.name = hfmToHifiJointNameMapping.key(fbxModel.name); - } joint.bindTransformFoundInCluster = false; hfmModel.joints.append(joint); - hfmModel.jointIndices.insert(joint.name, hfmModel.joints.size()); QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); @@ -1718,21 +1674,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } - auto offsets = getJointRotationOffsets(mapping); - hfmModel.jointRotationOffsets.clear(); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - glm::quat rotationOffset = itr.value(); - int jointIndex = hfmModel.getJointIndex(jointName); - if (hfmToHifiJointNameMapping.contains(jointName)) { - jointIndex = hfmModel.getJointIndex(jointName); - } - if (jointIndex != -1) { - hfmModel.jointRotationOffsets.insert(jointIndex, rotationOffset); - } - qCDebug(modelformat) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; - } - return hfmModelPtr; } diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 8d1a82518d..1c2a2f5c63 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -19,13 +19,14 @@ #include "CalculateMeshTangentsTask.h" #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" +#include "PrepareJointsTask.h" namespace baker { class GetModelPartsTask { public: using Input = hfm::Model::Pointer; - using Output = VaryingSet5, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, QHash>; + using Output = VaryingSet6, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, QHash, std::vector>; using JobModel = Job::ModelIO; void run(const BakeContextPointer& context, const Input& input, Output& output) { @@ -39,6 +40,7 @@ namespace baker { blendshapesPerMesh.push_back(hfmModelIn->meshes[i].blendshapes.toStdVector()); } output.edit4() = hfmModelIn->materials; + output.edit5() = hfmModelIn->joints.toStdVector(); } }; @@ -99,23 +101,29 @@ namespace baker { class BuildModelTask { public: - using Input = VaryingSet2>; + using Input = VaryingSet5, std::vector, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using Output = hfm::Model::Pointer; using JobModel = Job::ModelIO; void run(const BakeContextPointer& context, const Input& input, Output& output) { auto hfmModelOut = input.get0(); hfmModelOut->meshes = QVector::fromStdVector(input.get1()); + hfmModelOut->joints = QVector::fromStdVector(input.get2()); + hfmModelOut->jointRotationOffsets = input.get3(); + hfmModelOut->jointIndices = input.get4(); output = hfmModelOut; } }; class BakerEngineBuilder { public: - using Input = hfm::Model::Pointer; + using Input = VaryingSet2; using Output = hfm::Model::Pointer; using JobModel = Task::ModelIO; - void build(JobModel& model, const Varying& hfmModelIn, Varying& hfmModelOut) { + void build(JobModel& model, const Varying& input, Varying& hfmModelOut) { + const auto& hfmModelIn = input.getN(0); + const auto& mapping = input.getN(1); + // Split up the inputs from hfm::Model const auto modelPartsIn = model.addJob("GetModelParts", hfmModelIn); const auto meshesIn = modelPartsIn.getN(0); @@ -123,6 +131,7 @@ namespace baker { const auto meshIndicesToModelNames = modelPartsIn.getN(2); const auto blendshapesPerMeshIn = modelPartsIn.getN(3); const auto materials = modelPartsIn.getN(4); + const auto jointsIn = modelPartsIn.getN(5); // Calculate normals and tangents for meshes and blendshapes if they do not exist // Note: Normals are never calculated here for OBJ models. OBJ files optionally define normals on a per-face basis, so for consistency normals are calculated beforehand in OBJSerializer. @@ -138,19 +147,27 @@ namespace baker { const auto buildGraphicsMeshInputs = BuildGraphicsMeshTask::Input(meshesIn, url, meshIndicesToModelNames, normalsPerMesh, tangentsPerMesh).asVarying(); const auto graphicsMeshes = model.addJob("BuildGraphicsMesh", buildGraphicsMeshInputs); + // Prepare joint information + const auto prepareJointsInputs = PrepareJointsTask::Input(jointsIn, mapping).asVarying(); + const auto jointInfoOut = model.addJob("PrepareJoints", prepareJointsInputs); + const auto jointsOut = jointInfoOut.getN(0); + const auto jointRotationOffsets = jointInfoOut.getN(1); + const auto jointIndices = jointInfoOut.getN(2); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying(); const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs); - const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut).asVarying(); + const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); hfmModelOut = model.addJob("BuildModel", buildModelInputs); } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { - _engine->feedInput(hfmModel); + _engine->feedInput(0, hfmModel); + _engine->feedInput(1, mapping); } void Baker::run() { diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 7fb3f420e0..41989d73df 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -12,6 +12,8 @@ #ifndef hifi_baker_Baker_h #define hifi_baker_Baker_h +#include + #include #include "Engine.h" @@ -19,7 +21,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel); + Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping); void run(); diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp new file mode 100644 index 0000000000..82ad77d651 --- /dev/null +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -0,0 +1,89 @@ +// +// PrepareJointsTask.cpp +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/01/25. +// Copyright 2019 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 "PrepareJointsTask.h" + +#include "ModelBakerLogging.h" + +QMap getJointNameMapping(const QVariantHash& mapping) { + static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; + QMap hfmToHifiJointNameMap; + if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { + auto jointNames = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); + for (auto itr = jointNames.begin(); itr != jointNames.end(); itr++) { + hfmToHifiJointNameMap.insert(itr.key(), itr.value().toString()); + qCDebug(model_baker) << "the mapped key " << itr.key() << " has a value of " << hfmToHifiJointNameMap[itr.key()]; + } + } + return hfmToHifiJointNameMap; +} + +QMap getJointRotationOffsets(const QVariantHash& mapping) { + QMap jointRotationOffsets; + static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; + if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { + auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + QString line = itr.value().toString(); + auto quatCoords = line.split(','); + if (quatCoords.size() == 4) { + float quatX = quatCoords[0].mid(1).toFloat(); + float quatY = quatCoords[1].toFloat(); + float quatZ = quatCoords[2].toFloat(); + float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat(); + if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) { + glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ); + jointRotationOffsets.insert(jointName, rotationOffset); + } + } + } + } + return jointRotationOffsets; +} + +void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& jointsIn = input.get0(); + const auto& mapping = input.get1(); + auto& jointsOut = output.edit0(); + auto& jointRotationOffsets = output.edit1(); + auto& jointIndices = output.edit2(); + + // Get which joints are free from FST file mappings + QVariantList freeJoints = mapping.values("freeJoint"); + // Get joint renames + auto jointNameMapping = getJointNameMapping(mapping); + // Apply joint metadata from FST file mappings + for (const auto& jointIn : jointsIn) { + jointsOut.push_back(jointIn); + auto& jointOut = jointsOut[jointsOut.size()-1]; + + jointOut.isFree = freeJoints.contains(jointIn.name); + + if (jointNameMapping.contains(jointNameMapping.key(jointIn.name))) { + jointOut.name = jointNameMapping.key(jointIn.name); + } + + jointIndices.insert(jointOut.name, (int)jointsOut.size()); + } + + // Get joint rotation offsets from FST file mappings + auto offsets = getJointRotationOffsets(mapping); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + glm::quat rotationOffset = itr.value(); + int jointIndex = jointIndices.value(jointName) - 1; + if (jointIndex != -1) { + jointRotationOffsets.insert(jointIndex, rotationOffset); + } + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + } +} diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h new file mode 100644 index 0000000000..e12d8ffd2c --- /dev/null +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -0,0 +1,30 @@ +// +// PrepareJointsTask.h +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/01/25. +// Copyright 2019 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_PrepareJointsTask_h +#define hifi_PrepareJointsTask_h + +#include + +#include + +#include "Engine.h" + +class PrepareJointsTask { +public: + using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; + using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; + using JobModel = baker::Job::ModelIO; + + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_PrepareJointsTask_h \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 05c4aa0e03..f18d4ab28c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -233,7 +233,7 @@ void GeometryReader::run() { } QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(HFMModel::Pointer, hfmModel)); + Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(QVariantHash, _mapping)); } catch (const std::exception&) { auto resource = _resource.toStrongRef(); if (resource) { @@ -261,7 +261,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; protected: - Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel); + Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping); private: ModelLoader _modelLoader; @@ -277,9 +277,9 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType())); } -void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel) { +void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping) { // Do processing on the model - baker::Baker modelBaker(hfmModel); + baker::Baker modelBaker(hfmModel, mapping); modelBaker.run(); // Assume ownership of the processed HFMModel From eace901278335e413d37485bb3e20f52434501a9 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 29 Jan 2019 10:29:12 -0800 Subject: [PATCH 026/130] Fix not calculating joint freeLineage list properly --- libraries/fbx/src/FBXSerializer.cpp | 12 +----------- .../src/model-baker/PrepareJointsTask.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 92d5e7e774..207ee2982d 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1252,18 +1252,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; joint.parentIndex = fbxModel.parentIndex; - - // get the indices of all ancestors starting with the first free one (if any) int jointIndex = hfmModel.joints.size(); - joint.freeLineage.append(jointIndex); - int lastFreeIndex = joint.isFree ? 0 : -1; - for (int index = joint.parentIndex; index != -1; index = hfmModel.joints.at(index).parentIndex) { - if (hfmModel.joints.at(index).isFree) { - lastFreeIndex = joint.freeLineage.size(); - } - joint.freeLineage.append(index); - } - joint.freeLineage.remove(lastFreeIndex + 1, joint.freeLineage.size() - lastFreeIndex - 1); + joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 82ad77d651..5f4a1b4f04 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -67,6 +67,17 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu auto& jointOut = jointsOut[jointsOut.size()-1]; jointOut.isFree = freeJoints.contains(jointIn.name); + // Get the indices of all ancestors starting with the first free one (if any) + int jointIndex = jointsOut.size() - 1; + jointOut.freeLineage.append(jointIndex); + int lastFreeIndex = jointOut.isFree ? 0 : -1; + for (int index = jointOut.parentIndex; index != -1; index = jointsOut.at(index).parentIndex) { + if (jointsOut.at(index).isFree) { + lastFreeIndex = jointOut.freeLineage.size(); + } + jointOut.freeLineage.append(index); + } + jointOut.freeLineage.remove(lastFreeIndex + 1, jointOut.freeLineage.size() - lastFreeIndex - 1); if (jointNameMapping.contains(jointNameMapping.key(jointIn.name))) { jointOut.name = jointNameMapping.key(jointIn.name); From fb5ef95a5bbf8eaa4c5bb03dea3e9bf526a3ba4e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 29 Jan 2019 11:00:18 -0800 Subject: [PATCH 027/130] disable bf triangle collisions for flying MyAvatar --- interface/src/Application.cpp | 2 ++ libraries/physics/src/PhysicsEngine.cpp | 27 +++++++++++++++++++++++++ libraries/physics/src/PhysicsEngine.h | 2 ++ 3 files changed, 31 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 356065a9bb..135ada6312 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6289,6 +6289,8 @@ void Application::update(float deltaTime) { avatarManager->handleProcessedPhysicsTransaction(transaction); myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { dynamic->prepareForPhysicsSimulation(); }); diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index bf6e2463e5..deaad81296 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -27,6 +27,23 @@ #include "ThreadSafeDynamicsWorld.h" #include "PhysicsLogging.h" +static bool flipNormalsMyAvatarVsBackfacingTriangles(btManifoldPoint& cp, + const btCollisionObjectWrapper* colObj0Wrap, int partId0, int index0, + const btCollisionObjectWrapper* colObj1Wrap, int partId1, int index1) { + if (colObj1Wrap->getCollisionShape()->getShapeType() == TRIANGLE_SHAPE_PROXYTYPE) { + auto triShape = static_cast(colObj1Wrap->getCollisionShape()); + const btVector3* v = triShape->m_vertices1; + btVector3 faceNormal = colObj1Wrap->getWorldTransform().getBasis() * btCross(v[1] - v[0], v[2] - v[0]); + float nDotF = btDot(faceNormal, cp.m_normalWorldOnB); + if (nDotF <= 0.0f) { + faceNormal.normalize(); + // flip the contact normal to be aligned with the face normal + cp.m_normalWorldOnB += -2.0f * nDotF * faceNormal; + } + } + // return value is currently ignored but to be future-proof: return false when not modifying friction + return false; +} PhysicsEngine::PhysicsEngine(const glm::vec3& offset) : _originOffset(offset), @@ -904,6 +921,16 @@ void PhysicsEngine::setShowBulletConstraintLimits(bool value) { } } +void PhysicsEngine::enableGlobalContactAddedCallback(bool enabled) { + if (enabled) { + // register contact filter to help MyAvatar pass through backfacing triangles + gContactAddedCallback = flipNormalsMyAvatarVsBackfacingTriangles; + } else { + // deregister contact filter + gContactAddedCallback = nullptr; + } +} + struct AllContactsCallback : public btCollisionWorld::ContactResultCallback { AllContactsCallback(int32_t mask, int32_t group, const ShapeInfo& shapeInfo, const Transform& transform, btCollisionObject* myAvatarCollisionObject, float threshold) : btCollisionWorld::ContactResultCallback(), diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index d10be018b8..43cc0d2176 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -148,6 +148,8 @@ public: // See PhysicsCollisionGroups.h for mask flags. std::vector contactTest(uint16_t mask, const ShapeInfo& regionShapeInfo, const Transform& regionTransform, uint16_t group = USER_COLLISION_GROUP_DYNAMIC, float threshold = 0.0f) const; + void enableGlobalContactAddedCallback(bool enabled); + private: QList removeDynamicsForBody(btRigidBody* body); void addObjectToDynamicsWorld(ObjectMotionState* motionState); From e372cb668ad19068d480c6d923e93f03cc336d46 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 29 Jan 2019 11:11:54 -0800 Subject: [PATCH 028/130] enable CCD for MyAvatar's RigidBody --- libraries/physics/src/CharacterController.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index d5ded6f909..2eceb226c9 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -124,6 +124,11 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { _rigidBody->setGravity(_currentGravity * _currentUp); // set flag to enable custom contactAddedCallback _rigidBody->setCollisionFlags(btCollisionObject::CF_CUSTOM_MATERIAL_CALLBACK); + + // enable CCD + _rigidBody->setCcdSweptSphereRadius(_radius); + _rigidBody->setCcdMotionThreshold(_radius); + btCollisionShape* shape = _rigidBody->getCollisionShape(); assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE); _ghost.setCharacterShape(static_cast(shape)); @@ -454,6 +459,12 @@ void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const // it's ok to change offset immediately -- there are no thread safety issues here _shapeLocalOffset = minCorner + 0.5f * scale; + + if (_rigidBody) { + // update CCD with new _radius + _rigidBody->setCcdSweptSphereRadius(_radius); + _rigidBody->setCcdMotionThreshold(_radius); + } } void CharacterController::setCollisionless(bool collisionless) { From 203e8e24556398596c2e0b4c669430a86a45b254 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 29 Jan 2019 11:55:35 -0800 Subject: [PATCH 029/130] Realize joint properties isFree and freeLineage are unused, so tear them out --- .../resources/meshes/defaultAvatar_full.fst | 4 --- interface/src/ModelPackager.cpp | 7 ---- interface/src/ModelPropertiesDialog.cpp | 34 ------------------- interface/src/ModelPropertiesDialog.h | 2 -- libraries/animation/src/AnimSkeleton.cpp | 2 -- .../src/avatars-renderer/SkeletonModel.cpp | 8 ----- .../src/avatars-renderer/SkeletonModel.h | 8 ----- libraries/fbx/src/FST.cpp | 5 --- libraries/fbx/src/FSTReader.cpp | 4 +-- libraries/fbx/src/FSTReader.h | 1 - libraries/fbx/src/GLTFSerializer.cpp | 3 -- libraries/fbx/src/OBJSerializer.cpp | 4 +-- libraries/hfm/src/hfm/HFM.h | 2 -- .../src/model-baker/PrepareJointsTask.cpp | 15 -------- libraries/render-utils/src/Model.cpp | 4 --- libraries/render-utils/src/Model.h | 3 -- 16 files changed, 3 insertions(+), 103 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full.fst b/interface/resources/meshes/defaultAvatar_full.fst index b47faeb8a8..aa679e319a 100644 --- a/interface/resources/meshes/defaultAvatar_full.fst +++ b/interface/resources/meshes/defaultAvatar_full.fst @@ -10,10 +10,6 @@ joint = jointRoot = Hips joint = jointLeftHand = LeftHand joint = jointRightHand = RightHand joint = jointHead = Head -freeJoint = LeftArm -freeJoint = LeftForeArm -freeJoint = RightArm -freeJoint = RightForeArm bs = JawOpen = mouth_Open = 1 bs = LipsFunnel = Oo = 1 bs = BrowsU_L = brow_Up = 1 diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 84325da473..db74b34d91 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -294,13 +294,6 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename } mapping.insert(JOINT_FIELD, joints); - - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - } // If there are no blendshape mappings, and we detect that this is likely a mixamo file, // then we can add the default mixamo to "faceshift" mappings diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 1bdb170b60..d67341990d 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -58,11 +58,6 @@ _hfmModel(hfmModel) form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); - form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); - QPushButton* newFreeJoint = new QPushButton("New Free Joint"); - _freeJoints->addWidget(newFreeJoint); - connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset); connect(buttons, SIGNAL(accepted()), SLOT(accept())); @@ -102,11 +97,6 @@ QVariantHash ModelPropertiesDialog::getMapping() const { insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); - mapping.remove(FREE_JOINT_FIELD); - for (int i = 0; i < _freeJoints->count() - 1; i++) { - QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); - mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); - } mapping.insert(JOINT_FIELD, joints); return mapping; @@ -133,16 +123,6 @@ void ModelPropertiesDialog::reset() { setJointText(_headJoint, jointHash.value("jointHead").toString()); setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); - - while (_freeJoints->count() > 1) { - delete _freeJoints->itemAt(0)->widget(); - } - foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { - QString jointName = joint.toString(); - if (_hfmModel.jointIndices.contains(jointName)) { - createNewFreeJoint(jointName); - } - } } void ModelPropertiesDialog::chooseTextureDirectory() { @@ -176,20 +156,6 @@ void ModelPropertiesDialog::updatePivotJoint() { _pivotJoint->setEnabled(!_pivotAboutCenter->isChecked()); } -void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { - QWidget* freeJoint = new QWidget(); - QHBoxLayout* freeJointLayout = new QHBoxLayout(); - freeJointLayout->setContentsMargins(QMargins()); - freeJoint->setLayout(freeJointLayout); - QComboBox* jointBox = createJointBox(false); - jointBox->setCurrentText(joint); - freeJointLayout->addWidget(jointBox, 1); - QPushButton* deleteJoint = new QPushButton("Delete"); - freeJointLayout->addWidget(deleteJoint); - freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); - _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); -} - QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { QComboBox* box = new QComboBox(); if (withNone) { diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 7019d239ff..8cf9bd5248 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -39,7 +39,6 @@ private slots: void chooseTextureDirectory(); void chooseScriptDirectory(); void updatePivotJoint(); - void createNewFreeJoint(const QString& joint = QString()); private: QComboBox* createJointBox(bool withNone = true) const; @@ -66,7 +65,6 @@ private: QComboBox* _headJoint = nullptr; QComboBox* _leftHandJoint = nullptr; QComboBox* _rightHandJoint = nullptr; - QVBoxLayout* _freeJoints = nullptr; }; #endif // hifi_ModelPropertiesDialog_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index cc48308f17..1e7b4f0c2c 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -289,8 +289,6 @@ void AnimSkeleton::dump(bool verbose) const { qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); if (verbose) { qCDebug(animation) << " hfmJoint ="; - 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; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 7f2dbda3de..ea71ff128c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -249,14 +249,6 @@ bool SkeletonModel::getRightHandPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getRightHandJointIndex(), position); } -bool SkeletonModel::getLeftShoulderPosition(glm::vec3& position) const { - return getJointPositionInWorldFrame(getLastFreeJointIndex(getLeftHandJointIndex()), position); -} - -bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { - return getJointPositionInWorldFrame(getLastFreeJointIndex(getRightHandJointIndex()), position); -} - bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { return isActive() && getJointPositionInWorldFrame(_rig.indexOfJoint("Head"), headPosition); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index ef0e1e0fae..99f6632306 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -57,17 +57,9 @@ public: /// \return true whether or not the position was found bool getRightHandPosition(glm::vec3& position) const; - /// Gets the position of the left shoulder. - /// \return whether or not the left shoulder joint was found - bool getLeftShoulderPosition(glm::vec3& position) const; - /// Returns the extended length from the left hand to its last free ancestor. float getLeftArmLength() const; - /// Gets the position of the right shoulder. - /// \return whether or not the right shoulder joint was found - bool getRightShoulderPosition(glm::vec3& position) const; - /// Returns the position of the head joint. /// \return whether or not the head was found bool getHeadPosition(glm::vec3& headPosition) const; diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 7828037c74..b6f109c217 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -82,11 +82,6 @@ FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePat } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - // If there are no blendshape mappings, and we detect that this is likely a mixamo file, // then we can add the default mixamo to "faceshift" mappings diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index 43806560dc..99087954eb 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD - << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD + << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; buffer.open(QIODevice::WriteOnly); @@ -92,7 +92,7 @@ QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { for (auto key : PREFERED_ORDER) { auto it = mapping.find(key); if (it != mapping.constEnd()) { - if (key == FREE_JOINT_FIELD || key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. + if (key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. for (auto multi : mapping.values(key)) { buffer.write(key.toUtf8()); buffer.write(" = "); diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 993d7c3148..ad952c4ed7 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -27,7 +27,6 @@ static const QString TRANSLATION_X_FIELD = "tx"; static const QString TRANSLATION_Y_FIELD = "ty"; static const QString TRANSLATION_Z_FIELD = "tz"; static const QString JOINT_FIELD = "joint"; -static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString BLENDSHAPE_FIELD = "bs"; static const QString SCRIPT_FIELD = "script"; static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 238e7d7fdb..69606b2738 100644 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -719,7 +719,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { //Build default joints hfmModel.joints.resize(1); - hfmModel.joints[0].isFree = false; hfmModel.joints[0].parentIndex = -1; hfmModel.joints[0].distanceToParent = 0; hfmModel.joints[0].translation = glm::vec3(0, 0, 0); @@ -1299,8 +1298,6 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " shapeInfo.dots =" << joint.shapeInfo.dots; qCDebug(modelformat) << " shapeInfo.points =" << joint.shapeInfo.points; - qCDebug(modelformat) << " isFree =" << joint.isFree; - qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent; qCDebug(modelformat) << " translation" << joint.translation; diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index 9d4b1f16a1..91d3fc7cc0 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -687,7 +687,6 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash mesh.meshIndex = 0; hfmModel.joints.resize(1); - hfmModel.joints[0].isFree = false; hfmModel.joints[0].parentIndex = -1; hfmModel.joints[0].distanceToParent = 0; hfmModel.joints[0].translation = glm::vec3(0, 0, 0); @@ -1048,8 +1047,7 @@ void hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " joints.count() =" << hfmModel.joints.count(); foreach (HFMJoint joint, hfmModel.joints) { - qCDebug(modelformat) << " isFree =" << joint.isFree; - qCDebug(modelformat) << " freeLineage" << joint.freeLineage; + qCDebug(modelformat) << " parentIndex" << joint.parentIndex; qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent; qCDebug(modelformat) << " translation" << joint.translation; diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 07528f3348..cccfaa3f7d 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -75,8 +75,6 @@ struct JointShapeInfo { class Joint { public: JointShapeInfo shapeInfo; - QVector freeLineage; - bool isFree; int parentIndex; float distanceToParent; diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 5f4a1b4f04..20715cfed7 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -57,8 +57,6 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu auto& jointRotationOffsets = output.edit1(); auto& jointIndices = output.edit2(); - // Get which joints are free from FST file mappings - QVariantList freeJoints = mapping.values("freeJoint"); // Get joint renames auto jointNameMapping = getJointNameMapping(mapping); // Apply joint metadata from FST file mappings @@ -66,19 +64,6 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu jointsOut.push_back(jointIn); auto& jointOut = jointsOut[jointsOut.size()-1]; - jointOut.isFree = freeJoints.contains(jointIn.name); - // Get the indices of all ancestors starting with the first free one (if any) - int jointIndex = jointsOut.size() - 1; - jointOut.freeLineage.append(jointIndex); - int lastFreeIndex = jointOut.isFree ? 0 : -1; - for (int index = jointOut.parentIndex; index != -1; index = jointsOut.at(index).parentIndex) { - if (jointsOut.at(index).isFree) { - lastFreeIndex = jointOut.freeLineage.size(); - } - jointOut.freeLineage.append(index); - } - jointOut.freeLineage.remove(lastFreeIndex + 1, jointOut.freeLineage.size() - lastFreeIndex - 1); - if (jointNameMapping.contains(jointNameMapping.key(jointIn.name))) { jointOut.name = jointNameMapping.key(jointIn.name); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index da8dceb176..0206bd6963 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1116,10 +1116,6 @@ int Model::getParentJointIndex(int jointIndex) const { return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).parentIndex : -1; } -int Model::getLastFreeJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).freeLineage.last() : -1; -} - void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { _needsFixupInScene = true; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 16e08c2b23..aadfca78ba 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -379,9 +379,6 @@ protected: /// Clear the joint states void clearJointState(int index); - /// Returns the index of the last free ancestor of the indexed joint, or -1 if not found. - int getLastFreeJointIndex(int jointIndex) const; - /// \param jointIndex index of joint in model structure /// \param position[out] position of joint in model-frame /// \return true if joint exists From 0fdbca8ade2d24f931b9e6537241cb36cab97cc0 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 29 Jan 2019 16:07:27 -0800 Subject: [PATCH 030/130] QmlMarketplace Bugfixes * Fix upgrades * Certificate 'View in Marketplace' wasn't working * command-line hifiapp:MARKET wasn't launching * Home link wasn't disappearing where it should * Log In button on marketplace wasn't working * Other minor UI bugfixes --- .../qml/hifi/commerce/checkout/Checkout.qml | 2 +- .../common/EmulatedMarketplaceHeader.qml | 163 +----------------- .../InspectionCertificate.qml | 12 +- .../hifi/commerce/marketplace/Marketplace.qml | 52 +++--- .../commerce/marketplace/MarketplaceItem.qml | 5 +- .../hifi/commerce/purchases/PurchasedItem.qml | 24 +-- .../qml/hifi/commerce/purchases/Purchases.qml | 59 +------ .../qml/hifi/commerce/wallet/WalletHome.qml | 2 +- scripts/system/commerce/wallet.js | 12 +- scripts/system/marketplaces/marketplaces.js | 26 ++- 10 files changed, 66 insertions(+), 291 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index c76f5a428a..2d5f77f006 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -662,7 +662,7 @@ Rectangle { anchors.right: parent.right; text: "Cancel" onClicked: { - sendToScript({method: 'checkout_cancelClicked', params: itemId}); + sendToScript({method: 'checkout_cancelClicked', itemId: itemId}); } } } diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 0d0af875d1..759d61b924 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -24,11 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; - property string referrerURL: (Account.metaverseServerURL + "/marketplace?"); - readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; - property alias usernameDropdownVisible: usernameDropdown.visible; - height: mainContainer.height + additionalDropdownHeight; + height: mainContainer.height; Connections { target: Commerce; @@ -93,77 +90,7 @@ Item { MouseArea { anchors.fill: parent; onClicked: { - sendToParent({method: "header_marketplaceImageClicked", referrerURL: root.referrerURL}); - } - } - } - - Item { - id: buttonAndUsernameContainer; - anchors.left: marketplaceHeaderImage.right; - anchors.leftMargin: 8; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 10; - anchors.right: securityImage.left; - anchors.rightMargin: 6; - - TextMetrics { - id: textMetrics; - font.family: "Raleway" - text: usernameText.text; - } - - Rectangle { - id: myUsernameButton; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - height: 40; - width: usernameText.width + 25; - color: "white"; - radius: 4; - border.width: 1; - border.color: hifi.colors.lightGray; - - // Username Text - RalewayRegular { - id: usernameText; - text: Account.username; - // Text size - size: 18; - // Style - color: hifi.colors.baseGray; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - width: Math.min(textMetrics.width + 25, 110); - // Anchors - anchors.centerIn: parent; - rightPadding: 10; - } - - HiFiGlyphs { - id: dropdownIcon; - text: hifi.glyphs.caratDn; - // Size - size: 50; - // Anchors - anchors.right: parent.right; - anchors.rightMargin: -14; - anchors.verticalCenter: parent.verticalCenter; - horizontalAlignment: Text.AlignHCenter; - // Style - color: hifi.colors.baseGray; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - usernameDropdown.visible = !usernameDropdown.visible; - } - onEntered: usernameText.color = hifi.colors.baseGrayShadow; - onExited: usernameText.color = hifi.colors.baseGray; + sendToParent({method: "header_marketplaceImageClicked"}); } } } @@ -205,92 +132,6 @@ Item { } } - Item { - id: usernameDropdown; - z: 998; - visible: false; - anchors.top: buttonAndUsernameContainer.bottom; - anchors.topMargin: -buttonAndUsernameContainer.anchors.bottomMargin; - anchors.right: buttonAndUsernameContainer.right; - height: childrenRect.height; - width: 150; - - Rectangle { - id: myItemsButton; - color: hifi.colors.white; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; - - RalewaySemiBold { - anchors.fill: parent; - text: "My Submissions" - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - size: 18; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - onEntered: { - myItemsButton.color = hifi.colors.blueHighlight; - } - onExited: { - myItemsButton.color = hifi.colors.white; - } - onClicked: { - sendToParent({method: "header_myItemsClicked"}); - } - } - } - - Rectangle { - id: logOutButton; - color: hifi.colors.white; - anchors.top: myItemsButton.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; - - RalewaySemiBold { - anchors.fill: parent; - text: "Log Out" - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - size: 18; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - onEntered: { - logOutButton.color = hifi.colors.blueHighlight; - } - onExited: { - logOutButton.color = hifi.colors.white; - } - onClicked: { - Account.logOut(); - } - } - } - } - - DropShadow { - z: 997; - visible: usernameDropdown.visible; - anchors.fill: usernameDropdown; - horizontalOffset: 3; - verticalOffset: 3; - radius: 8.0; - samples: 17; - color: "#80000000"; - source: usernameDropdown; - } } diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 232e17d851..8ca34af28a 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -22,7 +22,6 @@ Rectangle { HifiConstants { id: hifi; } id: root; - property string marketplaceUrl: ""; property string entityId: ""; property string certificateId: ""; property string itemName: "--"; @@ -30,6 +29,7 @@ Rectangle { property string itemEdition: "--"; property string dateAcquired: "--"; property string itemCost: "--"; + property string marketplace_item_id: ""; property string certTitleTextColor: hifi.colors.darkGray; property string certTextColor: hifi.colors.white; property string infoTextColor: hifi.colors.blueAccent; @@ -69,7 +69,7 @@ Rectangle { errorText.text = "Information about this certificate is currently unavailable. Please try again later."; } } else { - root.marketplaceUrl = result.data.marketplace_item_url; + root.marketplace_item_id = result.data.marketplace_item_id; root.isMyCert = result.isMyCert ? result.isMyCert : false; if (root.certInfoReplaceMode > 3) { @@ -352,7 +352,7 @@ Rectangle { anchors.fill: parent; hoverEnabled: enabled; onClicked: { - sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplace_item_id}); } onEntered: itemName.color = hifi.colors.blueHighlight; onExited: itemName.color = root.certTextColor; @@ -391,7 +391,7 @@ Rectangle { // "Show In Marketplace" button HifiControlsUit.Button { id: showInMarketplaceButton; - enabled: root.marketplaceUrl; + enabled: root.marketplace_item_id && marketplace_item_id !== ""; color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; @@ -401,7 +401,7 @@ Rectangle { height: 40; text: "View In Market" onClicked: { - sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplace_item_id}); } } } @@ -620,7 +620,7 @@ Rectangle { root.itemOwner = "--"; root.itemEdition = "--"; root.dateAcquired = "--"; - root.marketplaceUrl = ""; + root.marketplace_item_id = ""; root.itemCost = "--"; root.isMyCert = false; errorText.text = ""; diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 8ba5d0bac0..d32d298acd 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -131,7 +131,6 @@ Rectangle { onLoginStatusResult: { root.isLoggedIn = isLoggedIn; - itemsLoginStatus.visible = !isLoggedIn; } } @@ -179,38 +178,27 @@ Rectangle { anchors.left: parent.left anchors.top: parent.top width: parent.width - height: 50 + height: 60 visible: true Image { - id: marketplaceIcon + id: marketplaceHeaderImage; + source: "../common/images/marketplaceHeaderImage.png"; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 0; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 140; + fillMode: Image.PreserveAspectFit; - anchors { - left: parent.left - leftMargin: 8 - verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent; + onClicked: { + sendToParent({method: "header_marketplaceImageClicked"}); + } } - height: 20 - width: marketplaceIcon.height - source: "../../../../images/hifi-logo-blackish.svg" - visible: true - } - - RalewaySemiBold { - id: titleBarText - - anchors { - top: parent.top - left: marketplaceIcon.right - bottom: parent.bottom - leftMargin: 6 - } - width: paintedWidth - - text: "Marketplace" - size: hifi.fontSizes.overlayTitle - color: hifi.colors.black - verticalAlignment: Text.AlignVCenter } } @@ -403,7 +391,7 @@ Rectangle { anchors.fill: parent anchors.rightMargin: 10 width: parent.width - + currentIndex: -1; clip: true model: categoriesModel @@ -587,7 +575,7 @@ Rectangle { left: parent.left right: parent.right leftMargin: 15 - rightMargin: 15 + top: parent.top+15 } height: root.isLoggedIn ? 0 : 80 @@ -944,7 +932,7 @@ Rectangle { isLoggedIn: root.isLoggedIn; onBuy: { - sendToScript({method: 'marketplace_checkout', itemId: item_id}); + sendToScript({method: 'marketplace_checkout', itemId: item_id, itemEdition: edition}); } onShowLicense: { @@ -1142,7 +1130,7 @@ Rectangle { console.log("A message with method 'updateMarketplaceQMLItem' was sent without an itemId!"); return; } - + marketplaceItem.edition = message.params.edition ? message.params.edition : -1; MarketplaceScriptingInterface.getMarketplaceItem(message.params.itemId); break; } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 5795d0d67d..0478f38764 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -41,6 +41,7 @@ Rectangle { property bool available: false property string created_at: "" property bool isLoggedIn: false; + property int edition: -1; onCategoriesChanged: { categoriesListModel.clear(); @@ -228,8 +229,8 @@ Rectangle { } height: 50 - text: root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)" - enabled: root.available + text: root.edition >= 0 ? "UPGRADE FOR FREE" : (root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)") + enabled: root.edition >= 0 || root.available buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : "" color: hifi.buttons.blue diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index f7dc26df5f..df6e216b32 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -28,6 +28,7 @@ Item { property string purchaseStatus; property string itemName; property string itemId; + property string updateItemId; property string itemPreviewImageUrl; property string itemHref; property string certificateId; @@ -45,9 +46,9 @@ Item { property bool cardBackVisible; property bool isInstalled; property string wornEntityID; - property string upgradeUrl; + property string updatedItemId; property string upgradeTitle; - property bool updateAvailable: root.upgradeUrl !== ""; + property bool updateAvailable: root.updateItemId && root.updateItemId !== ""; property bool valid; property string originalStatusText; @@ -175,7 +176,7 @@ Item { Item { property alias buttonGlyphText: buttonGlyph.text; - property alias buttonText: buttonText.text; + property alias itemButtonText: buttonText.text; property alias glyphSize: buttonGlyph.size; property string buttonColor: hifi.colors.black; property string buttonColor_hover: hifi.colors.blueHighlight; @@ -243,7 +244,7 @@ Item { onLoaded: { item.enabled = root.valid; item.buttonGlyphText = hifi.glyphs.gift; - item.buttonText = "Gift"; + item.itemButtonText = "Gift"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({ @@ -270,7 +271,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.market; - item.buttonText = "View in Marketplace"; + item.itemButtonText = "View in Marketplace"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); @@ -288,7 +289,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.certificate; - item.buttonText = "View Certificate"; + item.itemButtonText = "View Certificate"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({method: 'purchases_itemCertificateClicked', itemCertificateId: root.certificateId}); @@ -307,7 +308,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.uninstall; - item.buttonText = "Uninstall"; + item.itemButtonText = "Uninstall"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); Commerce.uninstallApp(root.itemHref); @@ -330,15 +331,14 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.update; - item.buttonText = "Update"; + item.itemButtonText = "Update"; item.buttonColor = "#E2334D"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({ method: 'updateItemClicked', - itemId: root.itemId, + itemId: root.updateAvailable ? root.updateItemId : root.itemId, itemEdition: root.itemEdition, - upgradeUrl: root.upgradeUrl, itemHref: root.itemHref, itemType: root.itemType, isInstalled: root.isInstalled, @@ -378,10 +378,10 @@ Item { function updateProperties() { if (updateButton.visible && uninstallButton.visible) { - item.buttonText = ""; + item.itemButtonText = ""; item.glyphSize = 20; } else { - item.buttonText = "Send to Trash"; + item.itemButtonText = "Send to Trash"; item.glyphSize = 30; } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 9433618b6b..bcc2a2821c 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -29,7 +29,6 @@ Rectangle { id: root; property string activeView: "initialize"; - property string referrerURL: ""; property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; @@ -154,55 +153,10 @@ Rectangle { } } - // - // TITLE BAR START - // - HifiCommerceCommon.EmulatedMarketplaceHeader { - id: titleBarContainer; - z: 997; - visible: false; - height: 100; - // Size - width: parent.width; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - Connections { - onSendToParent: { - if (msg.method === 'needsLogIn' && root.activeView !== "needsLogIn") { - root.activeView = "needsLogIn"; - } else if (msg.method === 'showSecurityPicLightbox') { - lightboxPopup.titleText = "Your Security Pic"; - lightboxPopup.bodyImageSource = msg.securityImageSource; - lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; - lightboxPopup.button1text = "CLOSE"; - lightboxPopup.button1method = function() { - lightboxPopup.visible = false; - } - lightboxPopup.visible = true; - } else { - sendToScript(msg); - } - } - } - } - MouseArea { - enabled: titleBarContainer.usernameDropdownVisible; - anchors.fill: parent; - onClicked: { - titleBarContainer.usernameDropdownVisible = false; - } - } - // - // TITLE BAR END - // - Rectangle { id: initialize; visible: root.activeView === "initialize"; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; @@ -219,8 +173,7 @@ Rectangle { id: installedAppsContainer; z: 998; visible: false; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width; @@ -422,8 +375,8 @@ Rectangle { // Anchors anchors.left: parent.left; anchors.right: parent.right; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: 8 - titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; + anchors.topMargin: 8; anchors.bottom: parent.bottom; // @@ -585,6 +538,7 @@ Rectangle { delegate: PurchasedItem { itemName: title; itemId: id; + updateItemId: model.upgrade_id ? model.upgrade_id : ""; itemPreviewImageUrl: preview; itemHref: download_url; certificateId: certificate_id; @@ -596,7 +550,6 @@ Rectangle { cardBackVisible: model.cardBackVisible || false; isInstalled: model.isInstalled || false; wornEntityID: model.wornEntityID; - upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; itemType: model.item_type; valid: model.valid; @@ -1083,8 +1036,6 @@ Rectangle { function fromScript(message) { switch (message.method) { case 'updatePurchases': - referrerURL = message.referrerURL || ""; - titleBarContainer.referrerURL = message.referrerURL || ""; filterBar.text = message.filterText ? message.filterText : ""; break; case 'purchases_showMyItems': diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index cf293a06df..eb8aa0f809 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -335,7 +335,7 @@ Item { if (link.indexOf("users/") !== -1) { sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); } else { - sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + sendSignalToWallet({method: 'transactionHistory_linkClicked', itemId: model.marketplace_item}); } } } diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 7cacfd7935..17ff918243 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -420,10 +420,10 @@ function fromQml(message) { case 'purchases': case 'marketplace cta': case 'mainPage': - ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; - default: // User needs to return to an individual marketplace item URL - ui.open(MARKETPLACE_URL + '/items/' + message.referrer, MARKETPLACES_INJECT_SCRIPT_URL); + default: + openMarketplace(); break; } break; @@ -435,13 +435,13 @@ function fromQml(message) { case 'maybeEnableHmdPreview': break; // do nothing here, handled in marketplaces.js case 'transactionHistory_linkClicked': - ui.open(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(message.itemId); break; case 'goToMarketplaceMainPage': - ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; case 'goToMarketplaceItemPage': - ui.open(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(message.itemId); break; case 'refreshConnections': print('Refreshing Connections...'); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 9d571d9284..b7a6b951a9 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,6 +19,7 @@ var selectionDisplay = null; // for gridTool.js to ignore var AppUi = Script.require('appUi'); Script.include("/~/system/libraries/gridTool.js"); Script.include("/~/system/libraries/connectionUtils.js"); +Script.include("/~/system/libraries/accountUtils.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; @@ -156,13 +157,12 @@ function onMarketplaceOpen(referrer) { } } -function openMarketplace(optionalItem) { +function openMarketplace(optionalItem, edition) { ui.open(MARKETPLACE_QML_PATH); - if (optionalItem) { ui.tablet.sendToQml({ method: 'updateMarketplaceQMLItem', - params: { itemId: optionalItem } + params: { itemId: optionalItem, edition: edition } }); } } @@ -486,7 +486,6 @@ function onWebEventReceived(message) { } else if (message.type === "WALLET_SETUP") { setupWallet('marketplace cta'); } else if (message.type === "MY_ITEMS") { - referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); @@ -519,7 +518,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { if (message.messageSrc === "HTML") { return; } - console.log(JSON.stringify(message)); switch (message.method) { case 'gotoBank': ui.close(); @@ -548,11 +546,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openWallet(); break; case 'checkout_cancelClicked': - openMarketplace(message.params); + openMarketplace(message.itemId); break; case 'header_goToPurchases': case 'checkout_goToPurchases': - referrerURL = MARKETPLACE_URL_INITIAL; filterText = message.filterText; ui.open(MARKETPLACE_PURCHASES_QML_PATH); break; @@ -602,13 +599,13 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } break; case 'header_marketplaceImageClicked': - openMarketplace(message.referrerURL); + openMarketplace(); break; case 'purchases_goToMarketplaceClicked': openMarketplace(); break; case 'updateItemClicked': - openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); + openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': @@ -638,10 +635,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { ContextOverlay.requestOwnershipVerification(message.entity); break; case 'inspectionCertificate_showInMarketplaceClicked': - openMarketplace(message.marketplaceUrl); + console.log("INSPECTION CERTIFICATE SHOW IN MARKETPLACE CLICKED: " + message.itemId); + openMarketplace(message.itemId); break; case 'header_myItemsClicked': - referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); @@ -750,11 +747,8 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { Keyboard.raised = false; } - if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { - ContextOverlay.isInMarketplaceInspectionMode = true; - } else { - ContextOverlay.isInMarketplaceInspectionMode = false; - } + ContextOverlay.isInMarketplaceInspectionMode = false; + if (onInspectionCertificateScreen) { setCertificateInfo(contextOverlayEntity); From 2446080a237e70faeb71632295f4217c967cba74 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:35:56 -0800 Subject: [PATCH 031/130] Removing dead code --- .../gvrinterface/InterfaceActivity.java | 42 ---- gvr-interface/CMakeLists.txt | 85 -------- gvr-interface/res/drawable/icon.png | Bin 9914 -> 0 bytes gvr-interface/src/Client.cpp | 73 ------- gvr-interface/src/Client.h | 33 --- gvr-interface/src/GVRInterface.cpp | 191 ------------------ gvr-interface/src/GVRInterface.h | 72 ------- gvr-interface/src/GVRMainWindow.cpp | 176 ---------------- gvr-interface/src/GVRMainWindow.h | 58 ------ gvr-interface/src/InterfaceView.cpp | 18 -- gvr-interface/src/InterfaceView.h | 23 --- gvr-interface/src/LoginDialog.cpp | 69 ------- gvr-interface/src/LoginDialog.h | 34 ---- gvr-interface/src/RenderingClient.cpp | 156 -------------- gvr-interface/src/RenderingClient.h | 57 ------ .../gvrinterface/InterfaceActivity.java | 41 ---- gvr-interface/src/main.cpp | 28 --- .../templates/InterfaceBetaActivity.java.in | 51 ----- gvr-interface/templates/hockeyapp.xml.in | 5 - 19 files changed, 1212 deletions(-) delete mode 100644 android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java delete mode 100644 gvr-interface/CMakeLists.txt delete mode 100644 gvr-interface/res/drawable/icon.png delete mode 100644 gvr-interface/src/Client.cpp delete mode 100644 gvr-interface/src/Client.h delete mode 100644 gvr-interface/src/GVRInterface.cpp delete mode 100644 gvr-interface/src/GVRInterface.h delete mode 100644 gvr-interface/src/GVRMainWindow.cpp delete mode 100644 gvr-interface/src/GVRMainWindow.h delete mode 100644 gvr-interface/src/InterfaceView.cpp delete mode 100644 gvr-interface/src/InterfaceView.h delete mode 100644 gvr-interface/src/LoginDialog.cpp delete mode 100644 gvr-interface/src/LoginDialog.h delete mode 100644 gvr-interface/src/RenderingClient.cpp delete mode 100644 gvr-interface/src/RenderingClient.h delete mode 100644 gvr-interface/src/java/io/highfidelity/gvrinterface/InterfaceActivity.java delete mode 100644 gvr-interface/src/main.cpp delete mode 100644 gvr-interface/templates/InterfaceBetaActivity.java.in delete mode 100644 gvr-interface/templates/hockeyapp.xml.in diff --git a/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java deleted file mode 100644 index aad769de70..0000000000 --- a/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java +++ /dev/null @@ -1,42 +0,0 @@ -// -// InterfaceActivity.java -// gvr-interface/java -// -// Created by Stephen Birarda on 1/26/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 -// - -package io.highfidelity.gvrinterface; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; -import android.util.Log; -import org.qtproject.qt5.android.bindings.QtActivity; - -public class InterfaceActivity extends QtActivity { - - public static native void handleHifiURL(String hifiURLString); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - // Get the intent that started this activity in case we have a hifi:// URL to parse - Intent intent = getIntent(); - if (intent.getAction() == Intent.ACTION_VIEW) { - Uri data = intent.getData(); - - if (data.getScheme().equals("hifi")) { - handleHifiURL(data.toString()); - } - } - - } -} \ No newline at end of file diff --git a/gvr-interface/CMakeLists.txt b/gvr-interface/CMakeLists.txt deleted file mode 100644 index 72f1096881..0000000000 --- a/gvr-interface/CMakeLists.txt +++ /dev/null @@ -1,85 +0,0 @@ -set(TARGET_NAME gvr-interface) - -if (ANDROID) - set(ANDROID_APK_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/apk-build") - set(ANDROID_APK_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/apk") - - set(ANDROID_SDK_ROOT $ENV{ANDROID_HOME}) - set(ANDROID_APP_DISPLAY_NAME Interface) - set(ANDROID_API_LEVEL 19) - set(ANDROID_APK_PACKAGE io.highfidelity.gvrinterface) - set(ANDROID_ACTIVITY_NAME io.highfidelity.gvrinterface.InterfaceActivity) - set(ANDROID_APK_VERSION_NAME "0.1") - set(ANDROID_APK_VERSION_CODE 1) - set(ANDROID_APK_FULLSCREEN TRUE) - set(ANDROID_DEPLOY_QT_INSTALL "--install") - - set(BUILD_SHARED_LIBS ON) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${ANDROID_APK_OUTPUT_DIR}/libs/${ANDROID_ABI}") - - setup_hifi_library(Gui AndroidExtras) -else () - setup_hifi_project(Gui) -endif () - -include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) - -link_hifi_libraries(shared networking audio-client avatars) - -if (ANDROID) - find_package(LibOVR) - - if (LIBOVR_FOUND) - add_definitions(-DHAVE_LIBOVR) - target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES} ${LIBOVR_ANDROID_LIBRARIES} ${TURBOJPEG_LIBRARY}) - include_directories(SYSTEM ${LIBOVR_INCLUDE_DIRS}) - - # we need VRLib, so add a project.properties to our apk build folder that says that - file(RELATIVE_PATH RELATIVE_VRLIB_PATH ${ANDROID_APK_OUTPUT_DIR} "${LIBOVR_VRLIB_DIR}") - file(WRITE "${ANDROID_APK_BUILD_DIR}/project.properties" "android.library.reference.1=${RELATIVE_VRLIB_PATH}") - - list(APPEND IGNORE_COPY_LIBS ${LIBOVR_ANDROID_LIBRARIES}) - endif () - -endif () - -# the presence of a HOCKEY_APP_ID means we are making a beta build -if (ANDROID AND HOCKEY_APP_ID) - set(HOCKEY_APP_ENABLED true) - set(HOCKEY_APP_ACTIVITY "\n") - set(ANDROID_ACTIVITY_NAME io.highfidelity.gvrinterface.InterfaceBetaActivity) - set(ANDROID_DEPLOY_QT_INSTALL "") - set(ANDROID_APK_CUSTOM_NAME "Interface-beta.apk") - - # set the ANDROID_APK_VERSION_CODE to the number of git commits - execute_process( - COMMAND git rev-list --first-parent --count HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_COUNT - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - set(ANDROID_APK_VERSION_CODE ${GIT_COMMIT_COUNT}) - - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/templates/InterfaceBetaActivity.java.in" "${ANDROID_APK_BUILD_DIR}/src/io/highfidelity/gvrinterface/InterfaceBetaActivity.java") -elseif (ANDROID) - set(HOCKEY_APP_ENABLED false) -endif () - -if (ANDROID) - - set(HIFI_URL_INTENT "\ - \n \ - \n \ - \n \ - \n \ - \n " - ) - - set(ANDROID_EXTRA_APPLICATION_XML "${HOCKEY_APP_ACTIVITY}") - set(ANDROID_EXTRA_ACTIVITY_XML "${HIFI_URL_INTENT}") - - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/templates/hockeyapp.xml.in" "${ANDROID_APK_BUILD_DIR}/res/values/hockeyapp.xml") - qt_create_apk() - -endif (ANDROID) diff --git a/gvr-interface/res/drawable/icon.png b/gvr-interface/res/drawable/icon.png deleted file mode 100644 index 70aaf9b4ed60a80081821907d53086beb249eb3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9914 zcmbWddpuO%7eBnt%rFyT3Q=grMP5-NQz2*x*%jKL~?6f ziexC%M|SBn$x40ga0Qu-0#%o!=2}H$VTA7fyrmDQ}n4)C1>Eo;iK^^vP3Kd_SMw z39#|RfxX5y0qw(`mbXqcC25Qg-H9CM(6v;5IQ@97|oFbH>4Jh1_)^0;pbeqX-GC0(CQU zLW-zioJuJXKM^{h73`%^tMW0~hk<1&dqNqGu|NP>^?B1sk@v?!*yqHB!lrNt4)QNv z=N=sw3iI-8qelV)>yC}f9C?a)1dhr|Uk<8Cp^v+W5WcF?0bM7}SDiGCjM{a}lPvtT z!|Db=)0V|=?aZChA?&MZO$z$Sw6Dk2=zuP);g|Ot?MuEA5gL6(R^MBn3=CZ@zIW;~ zZ#?B_ZhQqt%K%RTtB`LkKVMiRAvFXNXu5qHO9`VyaBp8s_mmCT$POhVAR;Yz=;b*l zuzqeFzSp*^awqB4IuQcAxm-vYTD0b64SheA=Sr+8xf@)>#FT^^=QnMjt`}t&kXDtC6UnIaa|v6jA1> z&FJ@%-3#>Y$Tbd@3WzZv5J4dzWQcn=?mzj0EkbMDNRnrp0)ie8EvpT-vdoOH4I{xV zyZPRB{tM|4cXP6Sw>(0CSv8*ccxb;bi;M%qHCWt;;cJ<7g6B_mxX2=?&3sAQu@dHI zce3)YjgIR8>d58?ok9Qeg#ec%kPwLaPWe`#T$2lo*K>6V}F*2YsiWF zve6hp4}M*=OpB*+w39v;T1YmoB!lJ<(GUNBinxOlMIR{XBDNxp4=%zVTb|`QZHX3Db*@-@RN(R*z^OQrC+}a@YWrw+9*Sn`=kS!bU z7$7!2Tp5M~*ELKzK3dE+&aLpmy>gpy{+#S|lo$FqntL)QjeB+ga!n^iQynwotMNL6k><4%3U^fS=+84!PUdOl8pI|J_+qkHxXp$y36}vB#t>jdw4Eu zZnJ+N+;oW|uu@fUXIe()U}E7Z(LOU;H499WSSUv$2Cbb)^=sMIK3$f^R1*XUVcsE77Tz*+PrDE z^S0HXmDm2(XCdw+8Kjt?2ncc9w&Pp^g>Ac})1{ zYtU!(4eY>*hg<^!R8r_9`u{441iO!-Q8aqimQ-#h30&(0y@$)P??HOf`9+1V;#-hm zC!Xys0zGSo%KxkJKXvzQL6E&wP%X_wP3(O)pHx7TqL@-4awhP2#O`-R-_Z|LP!+&# z6zUs#C@s-d#WO{$?! zr})xLbT(?;xW~&CIJ()#>$KlXcydA4S%js)#sZ$k1lHm2zDjAYSIcV0cU_>t)* z@D7sDa0fH{$%Wyb(83dI>!aYV3}&GwP@lsy^b$4^rXLF=&hgfx`6W|Al@t`WsPlZK zt#JNHNtOZzx1LGc3Tqp{uN9*PqOoUz|qji0MnKImat0=ngw`o%BKvIcQj0>o{f% z{V5K=mxLNGte`qh`hO0RKt%#rcVdN`v+;M;7Yv8^!t{NKhYAir-1b-=Vx?U0PCNIQ z8JwRGrAIXVX)Z5V0U4#MhXe{^+u)_$%hy?aD9~5VwJ-w}X(p(>y5Ho!iM2)nvY6nJ zvc?Q~C;|2uG@5Hezd{S zVQ8Hv(!bke7amoEUu^71yr%$he`4{{_n~#XGRR@z{^sC0(hujSp?MQA2V0pfooxcc zv2Em)7iN<5*qPpCr>Sv8<<~i;mta>s-(O1(OaISuz?J*ue9*9*@H69|K>}i1|5V*N z(#vP7<#3s=iCt&Mh+zM0wT|~FLQUr9oU-8ISw@czk9c~+{-Q;r!BU{tB%15w7AQc` zq7du0L0h8t+MPvk_usmrcSLZ{@UCTxYSxLPH3D#Fzx}DgP)Dc=i|n2PBCGG%;X_`B zh@2`K#JL_yyaJj_M0CL)>HmiK6w%;K^Orf?7i~yUVwO0afj3p(Y?S0Wkla}bkWRm+ zB?@jBa|Xg- zQWWQLb_9o-VZeG;;HQSL{C{;twb8)*r_g?#0$l}2z<^buQ!ov8?`RGOlmbgC>8!lO z&fDT>0Ta>#&TZ8J{aG1Edp6x0S~L0=^U7j4O+=-0B*1)MhE8EVbT!Z*?&V>P62x;Z=YUFg6~{cq>U-x>$j^XLbepQabvPF2t6a{QP*wsL< zFpMxE{rbar0{Zh_xHV6KFA7b$aUN5Rh7fs62W`MT2V%PUX0TBug57==2 zQ2;g*-0fso`@v_92I;Xzo<-sS18^eK{}f6xf@~RBoF~+UOS%SX5*;{%zwIO6wo`{aHvVU4*1=c0PVy6MKCVZi-+6O z5uG@C^*qp(E?pJ{$K|HRPF)!GD`Nm_?R8JS&tHRX9lD?oBzS!8YzcG$72FFeM{pd) zUL4%Myq^f*2aCf9pl&6XCFo)r#8i03+;ED*){G0{{ZxZUzIg!r=b!uj!Ql^coTX_3st$|5t$m z011G8A5X8-1bXH)0l@t%0N9)SpBY;jNy&DDzK#0|12-T z5G?Qn^m41LIE)ejMshRo0Ov-*PyIwQ0_YmqMqb2_Bm(urZP*M#mjQTu?qUgaZ2@pE zj5YvZ;p@*x0BCm<20>tz5Uk6Z%Ygc~KF0x2VY%#?7Op6xa~$YOjz@ZPB%m0e-n_IE z$EjNmo`yPqQ5dQs!n(7;MNqG`3+nfmD&Xnu3IlX1xId~L%9(X&?GKkH5ukQ5qg@>U zh5ET-UmR+3#qn>f>{0*&8Ff*BdTK}0Y=pwD%bh+5UCGPgyT8ca*~H|Y6VT`?QCQdg z>~H=J%R|FiVzj~Q$@P=^{#*dt8}*^ z092eaYapZUErNg*zbDz_@>vCxu@>NEmQU7tWzb(E`&U$#UnXvD^2L)pA@ty%RC~uy zf>mV!8=`wlf;lUl-)SuIXoA)n0Of_cvc>4(B5Z=);D#w(f`2at1=FvF+j4#H7{CKX z50B6u=X8^pzmD)eCG-f{6*!{ev<*~^%OAI!=>&Qxpt)zFyFbJ>zjFbrurQrVziuxs zRnI&1i0>&G#ostqSw~sSane!yUA08{dp_Wwk)5BPNp>4E!omewjg6oDM?3YtavKaU zjCbMUf%98`^_^)R_xCy_QmeW5%vIFbJuw#d-3HcN2#W*RzhPlPf9#$ z;>ID{F(b_Da^;1MGn&mijCwD_qsbR$Rf%dcf{yh|ZiU#_l9j@)l7#e|M3YX?zYO~o zyKeI`Tz^>4uOcwcyUbywy;plG2+WMOkr9AN;W+ z$mM^%xAQB5&eP|+Of#LVL6tS|t?a%Ob`wvZ>~W%yP4P1C?yrLUX41@L<8=WT;3~vs-^gizN`TTrOuyA-)$>gLx zwz^{Z@TV!;62B7?(+|SHDIr~a#Y_Y@BbbB>1EmuGh8e5@M;AeI`Yj!EL`7&MoOx}a z6Hg*`B4Rdn{!o%6wBGy>#E7Kx#xG!sB+cDfTPxphFU$OSFMO=uGTMnE6(U<#or7sh z;AjK150_k~xFe^7^Z3fli<$AY=qZO6vkC$8o+7kc1AhaGCkPJ%!=~1T2c5{kDP4Tw z`*Fi;89zc?)L~uhkTRr@$9D_-8i!KG@nMj9^FFpd%Zo@ALeC4PU$(qdc;RXxC3fM$=vmw`5{wF>RE?WOdWqw$bHh+lLd&$$J`~QZNg!CF(^2Eh; zw48~HX0GO0k1W`DG8@yI{e$~BIuY>waJjRpf2Y>mGi>ytW${Dp0x7+sV*bss+5#n? znGwBGvh++%8Fv2O5lXK`iSHn{U;BL||NCNfrCwqrI<-Kty&QZ$4arUWa{1YO$B0<( zC4%3`%S|CKq<2m(@baUWc5#zsQ-1d(qRi}pwbCU0%8M5Z!R9)UA{l)wIz^Ga6WTSG zH0;KX)$E+{He5>M$7~JU=`$?U%KUBgQ$b5pzaU3^q}&cS|KZ_Wse7Zv54_SWVpA#` zoLl%9yA|9elyEO|?A`HvKGS;yPZvRaw=-JIoNrf*HSS_iakE6v->>JEjOne}HyUF2 zNoU|j&sad$j+-kV6>Z5mr5C)Q7>*1N{8!`2XA?xeTE#zE9_0Y_LUGRB{vH%6sqw$% zfU4`!czhb_g{+X(}>2P*2@qB?m+ zSI_P1Cc6E3-r_Xqyla5>Xz{Dyw=@_W550)Az8&Wlzx%iIH7@|GgQ?XB)uKR>aB$nq zkl^=vI84${81an(sSrBZ{igoRSnDgS;ffGAqG7_3b3dk2BbR$Lmva1}UP#qxKB=8Z z!ri{SQA428KOs}61zXb8InP?0yNQogM*CJJ2<5!Xchf8^ig&{jrsiwrYNwo^M50F) zz8@GvBIls?(o9m(VyuJ+F8P9ihTuj2Ol+MYd>qCO3kFnqA~Y%~f4>HEr)EwY_G6jk z%?sb#3QH$_U7x%$j;W9!;j;X;Xb3W=c*3(sPfAUa$&P5M?zthMy8_ ze7PEmkAH|E9h*lki{mK$ie3DIreO)&pWYv6SJF%c$Ck5UIe+pG5$%zrXdE-zg(FM{@j`GM9zs@R^eZ5 zv*vBL!vw?-^Xb}JWecvwK=4(Cc4ntf-`VQ+L z5d+7Ej#x{FZJ+UzxfP+l0d=(!8iv_2tc}_4-Zj%4$NzLZjFUaq^RcX07*!R-_FFG| z*lLVQejysdTeSSWaPFfHCWqt9D|XrFK4&9RA*kC;AP>!4B;g(w_m^cOjF&}X>*ar5 zyy(P}4gq7?YE?sTRyN@gl+F`y$0Iy?aPfg|MbLdvea(P?3!G z^qdcOmBMbn>KZICfcyT42!-6(Zzzn@eEg=wxm-&Sd32GJyzvse)~=6eXx6X@Dg8PZ zBg`=Q89e&gHwqgxScA1M!?)$a6=f|~joo2oZ)~$tq zU5|%$2*)f`;OcRC6W+IO_PD(?E;u>_chBe94G77y3-I(0{2uRWd$nE1nmTxesd%zt z*HwXET79$^4PcK;KdtK5xcN(?g2MW{ExGlnp?Q=p8I|PmJ46<u@Bu=;YO?7Mwj{x~rTZCfXZn*j^ zJC>)joB+9xX<2I)>=JND`(RR;pvKo?$>ESr+O}DpiOLa$9kMS7>LWA0zRy{8*oTGBmkCtJ7Vcg@6@;YA$_Cu+);vNz*loej@5xka|JZ2WYe0W^)_{3c(#+0O8-+w0fW zJp6SGJ97Dh%-Ga-yp^jpuXns?Rb^Tk@p17ew@{qJIMUUPbI-tT|0WH_jrdM4_cvlUS3|1PhKf6>C&raUKnu(|s z&c)UAsZ85i1ZLC(dGSGFnHjqI|Lr#8szl_7-T0t%4bZAJ*s{4)wW?&u7HSA8C2>JbLGo`GY|V6fEdo z3^wDLb82a%sgJe!mS&T)(lCj`O(nt1B8#06_FapUEpzFzv&|tf#Z&o6W+W| zzDWU>d}8xY>J`$bsOxC?r5C3oP;ME&`OR-zyiN1%Ia>O>?Uyjc)?I{CLYAWP*U-6^ zV=Lu3QmO(RJ#`PKq|RtC5umb6cNl5iGxn)mUy0=~-K%1x()GvC`?M`LY03MZz=d36z9y*yqoWTr>8Xtj9RLYv9#lK;;? z8pPp}Znp3$_lN;hz%5%yZd=YT-?F_w8s@xjrHIp;43KX^`iB4geq&B{k6xiMC0ae% zQ|7Tj2vv(jUrbfIe5><~fcjI)!_!=IEoP-Nq+cc72oG~o&>?($g5o7K zc_S?zb|&CM)BawN%JdgLZb;w|Gtbs(&PxZ+mmjnbK0zFb?<@o z$0d?EJe|xarM>xL3GRL**#(hMvKf-R#HD?iwF3Lg*WFpI@7TrLelBkZfkqN%Zwcld`y( z>b&=o_O+teHYGL44Igt87oxreQ3&@5%%aV_lz<5pd(Y}N?aavY# z+d0HFXA7oGjsh!@Tz^23W%%{?-_43Y#QnY!b}3(;uP)##e_3ofGBLLHD1uIgb27Yc zg@=bdzZQB@d30Rj+bHd{jS!^C;i`U8WnFeqEmrxtD!S*yO?dCZyw*DZXF@~JwUOve zUDEZ+>i2Ej4WxY2FSD*$*q;vXhZB8X(~=P9N*+i%Pd}4bS9E~fuGr2W+cn4huk}|8 z)^y*EKV)(YSpA()gR3{s%Nq+XxX8a9qU<%PH6#P>E_&3DuQ^+5VoK~`UDtm#o^3&O zG>2m)+*i)P67EEM+v03RuBi%&XcMKwoDVg^QXZrv;m=T3WC6c*c zDm|A5f0UaST&tLUsRgg!rzi+;pd*e8KhNoTYkXYwUL~<#1?pc>>uWdu*dW_u z*WIyxDUAxXYCP-lbCZ(b9zfQANDJPaz&K7Pqg7G@_c>axtTTenUPA|P=I^#pN%V7~ zOuO2`=EWeB0wW+jAi8d99i$Y{HiptXs)yTln#5^i%_a}zGE&xMbK4z9pi z^Ln?0lJI(0CwAe@GxvGY?0MGnW(Jwek_zu|3JxTRq53qILvu_1PP)UPyjofpl|q1n z61dLBRjOM_du^$^Ia{3_d}rYHlM&{$M$WX}>^_M5cSgI{S+9o#jT=c~{zB&$8}`hK z)RXeku)FO8=b4F6X{d)(YE zpyw|pThFh8xHIH|Q_APp$gi;_|CmnAvHOSBAiPze$~)5jndYU#ek*~XQNp1Eq%Dv5 zGrj+X3RaiLmi;4U75@wyY6)rZp0I3ldpxl`sf_{YK_Kg{CfzJrz#2E?5E0b=dg#Mf zDN5JpMDTgeoCTaVYhaG)x7~F1AE>`et*=eXJvkm2ZiL7BY9!j|pITj%xTaUiu}e*s56i^q(P!f{2H&;7mOi@?LM0MV)xR@TxYG*G6@J1(_w=koAkHokr zF%zSEybEIhdLu$pr3n9!{rVS5_mbaqcLld4f4mqtAoV4uRib_IXkbZNTga;s?|=d_ zuzqN2zDe}S6X7256Pe{+u`Js&9xq59Yu>7wQD1$Y$DI)sl+9o#?#1(GgohTRcH!WQ zs@PtVzD>ke&gExXWQ8ze`uzQnqOLXFUB&-UJf|jj)tf2PXQslj?8Od^+G&mceb_tO z)d=)=tWE#8-Pk(3FT8x}{Z4r?i|t+&4HYaMEb?70rY=2Qj(JMiw z{V@88L-kopWoPuowoLD-V3zbVDO(Js22F-HSiCP;oJ|M{(a6gbwATeqJ>GLst#G~b z^%>rTi<=S!2aW1tThhpyB)r1&iuG)s@i%9G{CWboTTtx|8h2R*I%%tA6~>DQDs9FK zW>*1gUnnCt-sb?-eP20uzYz8=EC#@Q(8`R7=hYFJ zx@;l>e^!eLJBIBUyz+VAgsW2R9}V%^QRzT3u@cap?!&%a1BeI&y3 zDd+lR8Igc9Yy8x3I{CxW%JUs*_cd#~G*5hrJTp3$N|SIK{lEWcVJaM;h=d-vW20vZ P0B~TR#op}QjIjR)wK5d} diff --git a/gvr-interface/src/Client.cpp b/gvr-interface/src/Client.cpp deleted file mode 100644 index 8f064c7fd5..0000000000 --- a/gvr-interface/src/Client.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// -// Client.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/15. -// Copyright 2013 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 "Client.h" - -#include -#include -#include -#include -#include - -Client::Client(QObject* parent) : - QObject(parent) -{ - // we need to make sure that required dependencies are created - DependencyManager::set(); - - setupNetworking(); -} - -void Client::setupNetworking() { - // once Application order of instantiation is fixed this should be done from AccountManager - AccountManager::getInstance().setAuthURL(DEFAULT_NODE_AUTH_URL); - - // setup the NodeList for this client - DependencyManager::registerInheritance(); - auto nodeList = DependencyManager::set(NodeType::Agent, 0); - - // while datagram processing remains simple for targets using Client, we'll handle datagrams - connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &Client::processDatagrams); - - // every second, ask the NodeList to check in with the domain server - QTimer* domainCheckInTimer = new QTimer(this); - domainCheckInTimer->setInterval(DOMAIN_SERVER_CHECK_IN_MSECS); - connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); - - // TODO: once the Client knows its Address on start-up we should be able to immediately send a check in here - domainCheckInTimer->start(); - - // handle the case where the domain stops talking to us - // TODO: can we just have the nodelist do this when it sets up? Is there a user of the NodeList that wouldn't want this? - connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); -} - -void Client::processVerifiedPacket(const HifiSockAddr& senderSockAddr, const QByteArray& incomingPacket) { - DependencyManager::get()->processNodeData(senderSockAddr, incomingPacket); -} - -void Client::processDatagrams() { - HifiSockAddr senderSockAddr; - - static QByteArray incomingPacket; - - auto nodeList = DependencyManager::get(); - - while (DependencyManager::get()->getNodeSocket().hasPendingDatagrams()) { - incomingPacket.resize(nodeList->getNodeSocket().pendingDatagramSize()); - nodeList->getNodeSocket().readDatagram(incomingPacket.data(), incomingPacket.size(), - senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); - - if (nodeList->packetVersionAndHashMatch(incomingPacket)) { - processVerifiedPacket(senderSockAddr, incomingPacket); - } - } -} diff --git a/gvr-interface/src/Client.h b/gvr-interface/src/Client.h deleted file mode 100644 index 6fbe40f165..0000000000 --- a/gvr-interface/src/Client.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// Client.h -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/15. -// Copyright 2013 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_Client_h -#define hifi_Client_h - -#include - -#include - -class Client : public QObject { - Q_OBJECT -public: - Client(QObject* parent = 0); - - virtual void cleanupBeforeQuit() = 0; -protected: - - void setupNetworking(); - virtual void processVerifiedPacket(const HifiSockAddr& senderSockAddr, const QByteArray& incomingPacket); -private slots: - void processDatagrams(); -}; - -#endif // hifi_Client_h diff --git a/gvr-interface/src/GVRInterface.cpp b/gvr-interface/src/GVRInterface.cpp deleted file mode 100644 index f9a29d4ac4..0000000000 --- a/gvr-interface/src/GVRInterface.cpp +++ /dev/null @@ -1,191 +0,0 @@ -// -// GVRInterface.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 11/18/14. -// Copyright 2013 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 "GVRInterface.h" - -#ifdef ANDROID - -#include - -#include -#include -#include - -#ifdef HAVE_LIBOVR - -#include -#include - -#endif -#endif - -#include -#include -#include - -#include "GVRMainWindow.h" -#include "RenderingClient.h" - -static QString launchURLString = QString(); - -#ifdef ANDROID - -extern "C" { - -JNIEXPORT void Java_io_highfidelity_gvrinterface_InterfaceActivity_handleHifiURL(JNIEnv *jni, jclass clazz, jstring hifiURLString) { - launchURLString = QAndroidJniObject(hifiURLString).toString(); -} - -} - -#endif - -GVRInterface::GVRInterface(int argc, char* argv[]) : - QApplication(argc, argv), - _mainWindow(NULL), - _inVRMode(false) -{ - setApplicationName("gvr-interface"); - setOrganizationName("highfidelity"); - setOrganizationDomain("io"); - - if (!launchURLString.isEmpty()) { - // did we get launched with a lookup URL? If so it is time to give that to the AddressManager - qDebug() << "We were opened via a hifi URL -" << launchURLString; - } - - _client = new RenderingClient(this, launchURLString); - - launchURLString = QString(); - - connect(this, &QGuiApplication::applicationStateChanged, this, &GVRInterface::handleApplicationStateChange); - -#if defined(ANDROID) && defined(HAVE_LIBOVR) - QAndroidJniEnvironment jniEnv; - - QPlatformNativeInterface* interface = QApplication::platformNativeInterface(); - jobject activity = (jobject) interface->nativeResourceForIntegration("QtActivity"); - - ovr_RegisterHmtReceivers(&*jniEnv, activity); - - // PLATFORMACTIVITY_REMOVAL: Temp workaround for PlatformActivity being - // stripped from UnityPlugin. Alternate is to use LOCAL_WHOLE_STATIC_LIBRARIES - // but that increases the size of the plugin by ~1MiB - OVR::linkerPlatformActivity++; -#endif - - // call our idle function whenever we can - QTimer* idleTimer = new QTimer(this); - connect(idleTimer, &QTimer::timeout, this, &GVRInterface::idle); - idleTimer->start(0); - - // call our quit handler before we go down - connect(this, &QCoreApplication::aboutToQuit, this, &GVRInterface::handleApplicationQuit); -} - -void GVRInterface::handleApplicationQuit() { - _client->cleanupBeforeQuit(); -} - -void GVRInterface::idle() { -#if defined(ANDROID) && defined(HAVE_LIBOVR) - if (!_inVRMode && ovr_IsHeadsetDocked()) { - qDebug() << "The headset just got docked - enter VR mode."; - enterVRMode(); - } else if (_inVRMode) { - - if (ovr_IsHeadsetDocked()) { - static int counter = 0; - - // Get the latest head tracking state, predicted ahead to the midpoint of the time - // it will be displayed. It will always be corrected to the real values by - // time warp, but the closer we get, the less black will be pulled in at the edges. - const double now = ovr_GetTimeInSeconds(); - static double prev; - const double rawDelta = now - prev; - prev = now; - const double clampedPrediction = std::min( 0.1, rawDelta * 2); - ovrSensorState sensor = ovrHmd_GetSensorState(OvrHmd, now + clampedPrediction, true ); - - auto ovrOrientation = sensor.Predicted.Pose.Orientation; - glm::quat newOrientation(ovrOrientation.w, ovrOrientation.x, ovrOrientation.y, ovrOrientation.z); - _client->setOrientation(newOrientation); - - if (counter++ % 100000 == 0) { - qDebug() << "GetSensorState in frame" << counter << "-" - << ovrOrientation.x << ovrOrientation.y << ovrOrientation.z << ovrOrientation.w; - } - } else { - qDebug() << "The headset was undocked - leaving VR mode."; - - leaveVRMode(); - } - } - - OVR::KeyState& backKeyState = _mainWindow->getBackKeyState(); - auto backEvent = backKeyState.Update(ovr_GetTimeInSeconds()); - - if (backEvent == OVR::KeyState::KEY_EVENT_LONG_PRESS) { - qDebug() << "Attemping to start the Platform UI Activity."; - ovr_StartPackageActivity(_ovr, PUI_CLASS_NAME, PUI_GLOBAL_MENU); - } else if (backEvent == OVR::KeyState::KEY_EVENT_DOUBLE_TAP || backEvent == OVR::KeyState::KEY_EVENT_SHORT_PRESS) { - qDebug() << "Got an event we should cancel for!"; - } else if (backEvent == OVR::KeyState::KEY_EVENT_DOUBLE_TAP) { - qDebug() << "The button is down!"; - } -#endif -} - -void GVRInterface::handleApplicationStateChange(Qt::ApplicationState state) { - switch(state) { - case Qt::ApplicationActive: - qDebug() << "The application is active."; - break; - case Qt::ApplicationSuspended: - qDebug() << "The application is being suspended."; - break; - default: - break; - } -} - -void GVRInterface::enterVRMode() { -#if defined(ANDROID) && defined(HAVE_LIBOVR) - // Default vrModeParms - ovrModeParms vrModeParms; - vrModeParms.AsynchronousTimeWarp = true; - vrModeParms.AllowPowerSave = true; - vrModeParms.DistortionFileName = NULL; - vrModeParms.EnableImageServer = false; - vrModeParms.CpuLevel = 2; - vrModeParms.GpuLevel = 2; - vrModeParms.GameThreadTid = 0; - - QAndroidJniEnvironment jniEnv; - - QPlatformNativeInterface* interface = QApplication::platformNativeInterface(); - jobject activity = (jobject) interface->nativeResourceForIntegration("QtActivity"); - - vrModeParms.ActivityObject = activity; - - ovrHmdInfo hmdInfo; - _ovr = ovr_EnterVrMode(vrModeParms, &hmdInfo); - - _inVRMode = true; -#endif -} - -void GVRInterface::leaveVRMode() { -#if defined(ANDROID) && defined(HAVE_LIBOVR) - ovr_LeaveVrMode(_ovr); - _inVRMode = false; -#endif -} diff --git a/gvr-interface/src/GVRInterface.h b/gvr-interface/src/GVRInterface.h deleted file mode 100644 index 9ffbd52909..0000000000 --- a/gvr-interface/src/GVRInterface.h +++ /dev/null @@ -1,72 +0,0 @@ -// -// GVRInterface.h -// gvr-interface/src -// -// Created by Stephen Birarda on 11/18/14. -// Copyright 2013 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_GVRInterface_h -#define hifi_GVRInterface_h - -#include - -#if defined(ANDROID) && defined(HAVE_LIBOVR) -class ovrMobile; -class ovrHmdInfo; - -// This is set by JNI_OnLoad() when the .so is initially loaded. -// Must use to attach each thread that will use JNI: -namespace OVR { - // PLATFORMACTIVITY_REMOVAL: Temp workaround for PlatformActivity being - // stripped from UnityPlugin. Alternate is to use LOCAL_WHOLE_STATIC_LIBRARIES - // but that increases the size of the plugin by ~1MiB - extern int linkerPlatformActivity; -} - -#endif - -class GVRMainWindow; -class RenderingClient; -class QKeyEvent; - -#if defined(qApp) -#undef qApp -#endif -#define qApp (static_cast(QApplication::instance())) - -class GVRInterface : public QApplication { - Q_OBJECT -public: - GVRInterface(int argc, char* argv[]); - RenderingClient* getClient() { return _client; } - - void setMainWindow(GVRMainWindow* mainWindow) { _mainWindow = mainWindow; } - -protected: - void keyPressEvent(QKeyEvent* event); - -private slots: - void handleApplicationStateChange(Qt::ApplicationState state); - void idle(); -private: - void handleApplicationQuit(); - - void enterVRMode(); - void leaveVRMode(); - -#if defined(ANDROID) && defined(HAVE_LIBOVR) - ovrMobile* _ovr; - ovrHmdInfo* _hmdInfo; -#endif - - GVRMainWindow* _mainWindow; - - RenderingClient* _client; - bool _inVRMode; -}; - -#endif // hifi_GVRInterface_h diff --git a/gvr-interface/src/GVRMainWindow.cpp b/gvr-interface/src/GVRMainWindow.cpp deleted file mode 100644 index 5495354233..0000000000 --- a/gvr-interface/src/GVRMainWindow.cpp +++ /dev/null @@ -1,176 +0,0 @@ -// -// GVRMainWindow.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/14. -// Copyright 2013 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 "GVRMainWindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef ANDROID - -#include - -#elif defined(HAVE_LIBOVR) - -#include - -const float LIBOVR_DOUBLE_TAP_DURATION = 0.25f; -const float LIBOVR_LONG_PRESS_DURATION = 0.75f; - -#endif - -#include - -#include "InterfaceView.h" -#include "LoginDialog.h" -#include "RenderingClient.h" - - - -GVRMainWindow::GVRMainWindow(QWidget* parent) : - QMainWindow(parent), -#if defined(ANDROID) && defined(HAVE_LIBOVR) - _backKeyState(LIBOVR_DOUBLE_TAP_DURATION, LIBOVR_LONG_PRESS_DURATION), - _wasBackKeyDown(false), -#endif - _mainLayout(NULL), - _menuBar(NULL), - _loginAction(NULL) -{ - -#ifndef ANDROID - const int NOTE_4_WIDTH = 2560; - const int NOTE_4_HEIGHT = 1440; - setFixedSize(NOTE_4_WIDTH / 2, NOTE_4_HEIGHT / 2); -#endif - - setupMenuBar(); - - QWidget* baseWidget = new QWidget(this); - - // setup a layout so we can vertically align to top - _mainLayout = new QVBoxLayout(baseWidget); - _mainLayout->setAlignment(Qt::AlignTop); - - // set the layout on the base widget - baseWidget->setLayout(_mainLayout); - - setCentralWidget(baseWidget); - - // add the interface view - new InterfaceView(baseWidget); -} - -GVRMainWindow::~GVRMainWindow() { - delete _menuBar; -} - -void GVRMainWindow::keyPressEvent(QKeyEvent* event) { -#ifdef ANDROID - if (event->key() == Qt::Key_Back) { - // got the Android back key, hand off to OVR KeyState - _backKeyState.HandleEvent(ovr_GetTimeInSeconds(), true, (_wasBackKeyDown ? 1 : 0)); - _wasBackKeyDown = true; - return; - } -#endif - QWidget::keyPressEvent(event); -} - -void GVRMainWindow::keyReleaseEvent(QKeyEvent* event) { -#ifdef ANDROID - if (event->key() == Qt::Key_Back) { - // release on the Android back key, hand off to OVR KeyState - _backKeyState.HandleEvent(ovr_GetTimeInSeconds(), false, 0); - _wasBackKeyDown = false; - } -#endif - QWidget::keyReleaseEvent(event); -} - -void GVRMainWindow::setupMenuBar() { - QMenu* fileMenu = new QMenu("File"); - QMenu* helpMenu = new QMenu("Help"); - - _menuBar = new QMenuBar(0); - - _menuBar->addMenu(fileMenu); - _menuBar->addMenu(helpMenu); - - QAction* goToAddress = new QAction("Go to Address", fileMenu); - connect(goToAddress, &QAction::triggered, this, &GVRMainWindow::showAddressBar); - fileMenu->addAction(goToAddress); - - _loginAction = new QAction("Login", fileMenu); - fileMenu->addAction(_loginAction); - - // change the login action depending on our logged in/out state - AccountManager& accountManager = AccountManager::getInstance(); - connect(&accountManager, &AccountManager::loginComplete, this, &GVRMainWindow::refreshLoginAction); - connect(&accountManager, &AccountManager::logoutComplete, this, &GVRMainWindow::refreshLoginAction); - - // refresh the state now - refreshLoginAction(); - - QAction* aboutQt = new QAction("About Qt", helpMenu); - connect(aboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); - helpMenu->addAction(aboutQt); - - setMenuBar(_menuBar); -} - -void GVRMainWindow::showAddressBar() { - // setup the address QInputDialog - QInputDialog* addressDialog = new QInputDialog(this); - addressDialog->setLabelText("Address"); - - // add the address dialog to the main layout - _mainLayout->addWidget(addressDialog); - - connect(addressDialog, &QInputDialog::textValueSelected, - DependencyManager::get().data(), &AddressManager::handleLookupString); -} - -void GVRMainWindow::showLoginDialog() { - LoginDialog* loginDialog = new LoginDialog(this); - - // have the acccount manager handle credentials from LoginDialog - AccountManager& accountManager = AccountManager::getInstance(); - connect(loginDialog, &LoginDialog::credentialsEntered, &accountManager, &AccountManager::requestAccessToken); - connect(&accountManager, &AccountManager::loginFailed, this, &GVRMainWindow::showLoginFailure); - - _mainLayout->addWidget(loginDialog); -} - -void GVRMainWindow::showLoginFailure() { - QMessageBox::warning(this, "Login Failed", - "Could not log in with that username and password. Please try again!"); -} - -void GVRMainWindow::refreshLoginAction() { - AccountManager& accountManager = AccountManager::getInstance(); - disconnect(_loginAction, &QAction::triggered, &accountManager, 0); - - if (accountManager.isLoggedIn()) { - _loginAction->setText("Logout"); - connect(_loginAction, &QAction::triggered, &accountManager, &AccountManager::logout); - } else { - _loginAction->setText("Login"); - connect(_loginAction, &QAction::triggered, this, &GVRMainWindow::showLoginDialog); - } - -} diff --git a/gvr-interface/src/GVRMainWindow.h b/gvr-interface/src/GVRMainWindow.h deleted file mode 100644 index c28c19a6c1..0000000000 --- a/gvr-interface/src/GVRMainWindow.h +++ /dev/null @@ -1,58 +0,0 @@ -// -// GVRMainWindow.h -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/14. -// Copyright 2013 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_GVRMainWindow_h -#define hifi_GVRMainWindow_h - -#include - -#if defined(ANDROID) && defined(HAVE_LIBOVR) -#include -#endif - -class QKeyEvent; -class QMenuBar; -class QVBoxLayout; - -class GVRMainWindow : public QMainWindow { - Q_OBJECT -public: - GVRMainWindow(QWidget* parent = 0); - ~GVRMainWindow(); -public slots: - void showAddressBar(); - void showLoginDialog(); - - void showLoginFailure(); - -#if defined(ANDROID) && defined(HAVE_LIBOVR) - OVR::KeyState& getBackKeyState() { return _backKeyState; } -#endif - -protected: - void keyPressEvent(QKeyEvent* event); - void keyReleaseEvent(QKeyEvent* event); -private slots: - void refreshLoginAction(); -private: - void setupMenuBar(); - -#if defined(ANDROID) && defined(HAVE_LIBOVR) - OVR::KeyState _backKeyState; - bool _wasBackKeyDown; -#endif - - QVBoxLayout* _mainLayout; - QMenuBar* _menuBar; - QAction* _loginAction; -}; - -#endif // hifi_GVRMainWindow_h diff --git a/gvr-interface/src/InterfaceView.cpp b/gvr-interface/src/InterfaceView.cpp deleted file mode 100644 index e7992d3921..0000000000 --- a/gvr-interface/src/InterfaceView.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// -// InterfaceView.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 1/28/14. -// Copyright 2013 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 "InterfaceView.h" - -InterfaceView::InterfaceView(QWidget* parent, Qt::WindowFlags flags) : - QOpenGLWidget(parent, flags) -{ - -} \ No newline at end of file diff --git a/gvr-interface/src/InterfaceView.h b/gvr-interface/src/InterfaceView.h deleted file mode 100644 index 3d358a3e64..0000000000 --- a/gvr-interface/src/InterfaceView.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// InterfaceView.h -// gvr-interface/src -// -// Created by Stephen Birarda on 1/28/14. -// Copyright 2013 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_InterfaceView_h -#define hifi_InterfaceView_h - -#include - -class InterfaceView : public QOpenGLWidget { - Q_OBJECT -public: - InterfaceView(QWidget* parent = 0, Qt::WindowFlags flags = 0); -}; - -#endif // hifi_InterfaceView_h diff --git a/gvr-interface/src/LoginDialog.cpp b/gvr-interface/src/LoginDialog.cpp deleted file mode 100644 index d4efd425bd..0000000000 --- a/gvr-interface/src/LoginDialog.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// -// LoginDialog.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 2015-02-03. -// 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 "LoginDialog.h" - -#include -#include -#include -#include -#include - -LoginDialog::LoginDialog(QWidget* parent) : - QDialog(parent) -{ - setupGUI(); - setWindowTitle("Login"); - setModal(true); -} - -void LoginDialog::setupGUI() { - // setup a grid layout - QGridLayout* formGridLayout = new QGridLayout(this); - - _usernameLineEdit = new QLineEdit(this); - - QLabel* usernameLabel = new QLabel(this); - usernameLabel->setText("Username"); - usernameLabel->setBuddy(_usernameLineEdit); - - formGridLayout->addWidget(usernameLabel, 0, 0); - formGridLayout->addWidget(_usernameLineEdit, 1, 0); - - _passwordLineEdit = new QLineEdit(this); - _passwordLineEdit->setEchoMode(QLineEdit::Password); - - QLabel* passwordLabel = new QLabel(this); - passwordLabel->setText("Password"); - passwordLabel->setBuddy(_passwordLineEdit); - - formGridLayout->addWidget(passwordLabel, 2, 0); - formGridLayout->addWidget(_passwordLineEdit, 3, 0); - - QDialogButtonBox* buttons = new QDialogButtonBox(this); - - QPushButton* okButton = buttons->addButton(QDialogButtonBox::Ok); - QPushButton* cancelButton = buttons->addButton(QDialogButtonBox::Cancel); - - okButton->setText("Login"); - - connect(cancelButton, &QPushButton::clicked, this, &QDialog::close); - connect(okButton, &QPushButton::clicked, this, &LoginDialog::loginButtonClicked); - - formGridLayout->addWidget(buttons, 4, 0, 1, 2); - - setLayout(formGridLayout); -} - -void LoginDialog::loginButtonClicked() { - emit credentialsEntered(_usernameLineEdit->text(), _passwordLineEdit->text()); - close(); -} diff --git a/gvr-interface/src/LoginDialog.h b/gvr-interface/src/LoginDialog.h deleted file mode 100644 index 13f630818d..0000000000 --- a/gvr-interface/src/LoginDialog.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// LoginDialog.h -// gvr-interface/src -// -// Created by Stephen Birarda on 2015-02-03. -// 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_LoginDialog_h -#define hifi_LoginDialog_h - -#include - -class QLineEdit; - -class LoginDialog : public QDialog { - Q_OBJECT -public: - LoginDialog(QWidget* parent = 0); -signals: - void credentialsEntered(const QString& username, const QString& password); -private slots: - void loginButtonClicked(); -private: - void setupGUI(); - - QLineEdit* _usernameLineEdit; - QLineEdit* _passwordLineEdit; -}; - -#endif // hifi_LoginDialog_h \ No newline at end of file diff --git a/gvr-interface/src/RenderingClient.cpp b/gvr-interface/src/RenderingClient.cpp deleted file mode 100644 index 4c691a48e6..0000000000 --- a/gvr-interface/src/RenderingClient.cpp +++ /dev/null @@ -1,156 +0,0 @@ -// -// RenderingClient.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/15. -// Copyright 2013 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 "RenderingClient.h" - -#include -#include - -#include -#include -#include -#include - -RenderingClient* RenderingClient::_instance = NULL; - -RenderingClient::RenderingClient(QObject *parent, const QString& launchURLString) : - Client(parent) -{ - _instance = this; - - // connect to AddressManager and pass it the launch URL, if we have one - auto addressManager = DependencyManager::get(); - connect(addressManager.data(), &AddressManager::locationChangeRequired, this, &RenderingClient::goToLocation); - addressManager->loadSettings(launchURLString); - - // tell the NodeList which node types all rendering clients will want to know about - DependencyManager::get()->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer); - - DependencyManager::set(); - - // get our audio client setup on its own thread - auto audioClient = DependencyManager::set(); - audioClient->setPositionGetter(getPositionForAudio); - audioClient->setOrientationGetter(getOrientationForAudio); - audioClient->startThread(); - - - connect(&_avatarTimer, &QTimer::timeout, this, &RenderingClient::sendAvatarPacket); - _avatarTimer.setInterval(16); // 60 FPS - _avatarTimer.start(); - _fakeAvatar.setDisplayName("GearVR"); - _fakeAvatar.setFaceModelURL(QUrl(DEFAULT_HEAD_MODEL_URL)); - _fakeAvatar.setSkeletonModelURL(QUrl(DEFAULT_BODY_MODEL_URL)); - _fakeAvatar.toByteArray(); // Creates HeadData -} - -void RenderingClient::sendAvatarPacket() { - _fakeAvatar.setPosition(_position); - _fakeAvatar.setHeadOrientation(_orientation); - - QByteArray packet = byteArrayWithPopulatedHeader(PacketTypeAvatarData); - packet.append(_fakeAvatar.toByteArray()); - DependencyManager::get()->broadcastToNodes(packet, NodeSet() << NodeType::AvatarMixer); - _fakeAvatar.sendIdentityPacket(); -} - -void RenderingClient::cleanupBeforeQuit() { - DependencyManager::get()->cleanupBeforeQuit(); - // destroy the AudioClient so it and its thread will safely go down - DependencyManager::destroy(); -} - -void RenderingClient::processVerifiedPacket(const HifiSockAddr& senderSockAddr, const QByteArray& incomingPacket) { - auto nodeList = DependencyManager::get(); - PacketType incomingType = packetTypeForPacket(incomingPacket); - - switch (incomingType) { - case PacketTypeAudioEnvironment: - case PacketTypeAudioStreamStats: - case PacketTypeMixedAudio: - case PacketTypeSilentAudioFrame: { - - if (incomingType == PacketTypeAudioStreamStats) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "parseAudioStreamStatsPacket", - Qt::QueuedConnection, - Q_ARG(QByteArray, incomingPacket)); - } else if (incomingType == PacketTypeAudioEnvironment) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "parseAudioEnvironmentData", - Qt::QueuedConnection, - Q_ARG(QByteArray, incomingPacket)); - } else { - QMetaObject::invokeMethod(DependencyManager::get().data(), "addReceivedAudioToStream", - Qt::QueuedConnection, - Q_ARG(QByteArray, incomingPacket)); - } - - // update having heard from the audio-mixer and record the bytes received - SharedNodePointer audioMixer = nodeList->sendingNodeForPacket(incomingPacket); - - if (audioMixer) { - audioMixer->setLastHeardMicrostamp(usecTimestampNow()); - } - - break; - } - case PacketTypeBulkAvatarData: - case PacketTypeKillAvatar: - case PacketTypeAvatarIdentity: - case PacketTypeAvatarBillboard: { - // update having heard from the avatar-mixer and record the bytes received - SharedNodePointer avatarMixer = nodeList->sendingNodeForPacket(incomingPacket); - - if (avatarMixer) { - avatarMixer->setLastHeardMicrostamp(usecTimestampNow()); - - QMetaObject::invokeMethod(DependencyManager::get().data(), - "processAvatarMixerDatagram", - Q_ARG(const QByteArray&, incomingPacket), - Q_ARG(const QWeakPointer&, avatarMixer)); - } - break; - } - default: - Client::processVerifiedPacket(senderSockAddr, incomingPacket); - break; - } -} - -void RenderingClient::goToLocation(const glm::vec3& newPosition, - bool hasOrientationChange, const glm::quat& newOrientation, - bool shouldFaceLocation) { - qDebug().nospace() << "RenderingClient goToLocation - moving to " << newPosition.x << ", " - << newPosition.y << ", " << newPosition.z; - - glm::vec3 shiftedPosition = newPosition; - - if (hasOrientationChange) { - qDebug().nospace() << "RenderingClient goToLocation - new orientation is " - << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; - - // orient the user to face the target - glm::quat quatOrientation = newOrientation; - - if (shouldFaceLocation) { - - quatOrientation = newOrientation * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - shiftedPosition = newPosition - quatOrientation * glm::vec3( 0.0f, 0.0f,-1.0f) * DISTANCE_TO_USER; - } - - _orientation = quatOrientation; - } - - _position = shiftedPosition; - -} diff --git a/gvr-interface/src/RenderingClient.h b/gvr-interface/src/RenderingClient.h deleted file mode 100644 index c4724bc086..0000000000 --- a/gvr-interface/src/RenderingClient.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// RenderingClient.h -// gvr-interface/src -// -// Created by Stephen Birarda on 1/20/15. -// Copyright 2013 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_RenderingClient_h -#define hifi_RenderingClient_h - -#include -#include - -#include - -#include - -#include "Client.h" - -class RenderingClient : public Client { - Q_OBJECT -public: - RenderingClient(QObject* parent = 0, const QString& launchURLString = QString()); - - const glm::vec3& getPosition() const { return _position; } - const glm::quat& getOrientation() const { return _orientation; } - void setOrientation(const glm::quat& orientation) { _orientation = orientation; } - - static glm::vec3 getPositionForAudio() { return _instance->getPosition(); } - static glm::quat getOrientationForAudio() { return _instance->getOrientation(); } - - virtual void cleanupBeforeQuit(); - -private slots: - void goToLocation(const glm::vec3& newPosition, - bool hasOrientationChange, const glm::quat& newOrientation, - bool shouldFaceLocation); - void sendAvatarPacket(); - -private: - virtual void processVerifiedPacket(const HifiSockAddr& senderSockAddr, const QByteArray& incomingPacket); - - static RenderingClient* _instance; - - glm::vec3 _position; - glm::quat _orientation; - - QTimer _avatarTimer; - AvatarData _fakeAvatar; -}; - -#endif // hifi_RenderingClient_h diff --git a/gvr-interface/src/java/io/highfidelity/gvrinterface/InterfaceActivity.java b/gvr-interface/src/java/io/highfidelity/gvrinterface/InterfaceActivity.java deleted file mode 100644 index c7cbdd3dff..0000000000 --- a/gvr-interface/src/java/io/highfidelity/gvrinterface/InterfaceActivity.java +++ /dev/null @@ -1,41 +0,0 @@ -// -// InterfaceActivity.java -// gvr-interface/java -// -// Created by Stephen Birarda on 1/26/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 -// - -package io.highfidelity.gvrinterface; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.WindowManager; -import android.util.Log; -import org.qtproject.qt5.android.bindings.QtActivity; - -public class InterfaceActivity extends QtActivity { - - public static native void handleHifiURL(String hifiURLString); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - // Get the intent that started this activity in case we have a hifi:// URL to parse - Intent intent = getIntent(); - if (intent.getAction() == Intent.ACTION_VIEW) { - Uri data = intent.getData(); - - if (data.getScheme().equals("hifi")) { - handleHifiURL(data.toString()); - } - } - - } -} \ No newline at end of file diff --git a/gvr-interface/src/main.cpp b/gvr-interface/src/main.cpp deleted file mode 100644 index 26576393fb..0000000000 --- a/gvr-interface/src/main.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// -// main.cpp -// gvr-interface/src -// -// Created by Stephen Birarda on 11/17/14. -// 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 "GVRMainWindow.h" -#include "GVRInterface.h" - -int main(int argc, char* argv[]) { - GVRInterface app(argc, argv); - - GVRMainWindow mainWindow; -#ifdef ANDROID - mainWindow.showFullScreen(); -#else - mainWindow.showMaximized(); -#endif - - app.setMainWindow(&mainWindow); - - return app.exec(); -} \ No newline at end of file diff --git a/gvr-interface/templates/InterfaceBetaActivity.java.in b/gvr-interface/templates/InterfaceBetaActivity.java.in deleted file mode 100644 index 6698cfa409..0000000000 --- a/gvr-interface/templates/InterfaceBetaActivity.java.in +++ /dev/null @@ -1,51 +0,0 @@ -// -// InterfaceBetaActivity.java -// gvr-interface/java -// -// Created by Stephen Birarda on 1/27/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 -// - -package io.highfidelity.gvrinterface; - -import android.os.Bundle; -import net.hockeyapp.android.CrashManager; -import net.hockeyapp.android.UpdateManager; - -public class InterfaceBetaActivity extends InterfaceActivity { - - public String _hockeyAppID; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - _hockeyAppID = getString(R.string.HockeyAppID); - - checkForUpdates(); - } - - @Override - protected void onPause() { - super.onPause(); - UpdateManager.unregister(); - } - - @Override - protected void onResume() { - super.onResume(); - checkForCrashes(); - } - - private void checkForCrashes() { - CrashManager.register(this, _hockeyAppID); - } - - private void checkForUpdates() { - // Remove this for store / production builds! - UpdateManager.register(this, _hockeyAppID); - } -} \ No newline at end of file diff --git a/gvr-interface/templates/hockeyapp.xml.in b/gvr-interface/templates/hockeyapp.xml.in deleted file mode 100644 index edf2d0a8aa..0000000000 --- a/gvr-interface/templates/hockeyapp.xml.in +++ /dev/null @@ -1,5 +0,0 @@ - - - ${HOCKEY_APP_ID} - ${HOCKEY_APP_ENABLED} - From ab9a4a55f861f22d82d161749642da8d08008b1f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:40:29 -0800 Subject: [PATCH 032/130] Allow movement in frameplayer app --- tools/gpu-frame-player/src/PlayerWindow.cpp | 27 +++++++++++++- tools/gpu-frame-player/src/RenderThread.cpp | 39 ++++++++++++++++----- tools/gpu-frame-player/src/RenderThread.h | 2 ++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/tools/gpu-frame-player/src/PlayerWindow.cpp b/tools/gpu-frame-player/src/PlayerWindow.cpp index 7fbf43139f..e74caddd5e 100644 --- a/tools/gpu-frame-player/src/PlayerWindow.cpp +++ b/tools/gpu-frame-player/src/PlayerWindow.cpp @@ -64,11 +64,37 @@ void PlayerWindow::loadFrame() { } void PlayerWindow::keyPressEvent(QKeyEvent* event) { + bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); + float moveScale = isShifted ? 10.0f : 1.0f; switch (event->key()) { case Qt::Key_F1: loadFrame(); return; + case Qt::Key_W: + _renderThread.move(vec3{ 0, 0, -0.1f } * moveScale); + return; + + case Qt::Key_S: + _renderThread.move(vec3{ 0, 0, 0.1f } * moveScale); + return; + + case Qt::Key_A: + _renderThread.move(vec3{ -0.1f, 0, 0 } * moveScale); + return; + + case Qt::Key_D: + _renderThread.move(vec3{ 0.1f, 0, 0 } * moveScale); + return; + + case Qt::Key_E: + _renderThread.move(vec3{ 0, 0.1f, 0 } * moveScale); + return; + + case Qt::Key_F: + _renderThread.move(vec3{ 0, -0.1f, 0 } * moveScale); + return; + default: break; } @@ -106,5 +132,4 @@ void PlayerWindow::loadFrame(const QString& path) { } resize(size.x, size.y); } - _renderThread.submitFrame(frame); } diff --git a/tools/gpu-frame-player/src/RenderThread.cpp b/tools/gpu-frame-player/src/RenderThread.cpp index 608e8f250f..a9dcf8900f 100644 --- a/tools/gpu-frame-player/src/RenderThread.cpp +++ b/tools/gpu-frame-player/src/RenderThread.cpp @@ -20,6 +20,11 @@ void RenderThread::resize(const QSize& newSize) { _pendingSize.push(newSize); } +void RenderThread::move(const glm::vec3& v) { + std::unique_lock lock(_frameLock); + _correction = glm::inverse(glm::translate(mat4(), v)) * _correction; +} + void RenderThread::initialize(QWindow* window) { std::unique_lock lock(_frameLock); setObjectName("RenderThread"); @@ -105,6 +110,13 @@ void RenderThread::renderFrame(gpu::FramePointer& frame) { #ifdef USE_GL _context.makeCurrent(); #endif + if (_correction != glm::mat4()) { + std::unique_lock lock(_frameLock); + if (_correction != glm::mat4()) { + _backend->setCameraCorrection(_correction, _activeFrame->view); + //_prevRenderView = _correction * _activeFrame->view; + } + } _backend->recycle(); _backend->syncCache(); @@ -139,18 +151,29 @@ void RenderThread::renderFrame(gpu::FramePointer& frame) { using namespace vks::debug::marker; beginRegion(commandBuffer, "executeFrame", glm::vec4{ 1, 1, 1, 1 }); #endif + auto& glbackend = (gpu::gl::GLBackend&)(*_backend); + glm::uvec2 fboSize{ frame->framebuffer->getWidth(), frame->framebuffer->getHeight() }; + auto fbo = glbackend.getFramebufferID(frame->framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glClearColor(0, 0, 0, 1); + glClearDepth(0); + glClear(GL_DEPTH_BUFFER_BIT); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + //_gpuContext->enableStereo(true); if (frame && !frame->batches.empty()) { _gpuContext->executeFrame(frame); } #ifdef USE_GL - auto& glbackend = (gpu::gl::GLBackend&)(*_backend); - glm::uvec2 fboSize{ frame->framebuffer->getWidth(), frame->framebuffer->getHeight() }; - auto fbo = glbackend.getFramebufferID(frame->framebuffer); - glDisable(GL_FRAMEBUFFER_SRGB); - glBlitNamedFramebuffer(fbo, 0, 0, 0, fboSize.x, fboSize.y, 0, 0, windowSize.width(), windowSize.height(), - GL_COLOR_BUFFER_BIT, GL_NEAREST); + //glDisable(GL_FRAMEBUFFER_SRGB); + //glClear(GL_COLOR_BUFFER_BIT); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer( + 0, 0, fboSize.x, fboSize.y, + 0, 0, windowSize.width(), windowSize.height(), + GL_COLOR_BUFFER_BIT, GL_NEAREST); (void)CHECK_GL_ERROR(); _context.swapBuffers(); @@ -183,11 +206,11 @@ bool RenderThread::process() { pendingFrames.swap(_pendingFrames); pendingSize.swap(_pendingSize); } - + while (!pendingFrames.empty()) { _activeFrame = pendingFrames.front(); - _gpuContext->consumeFrameUpdates(_activeFrame); pendingFrames.pop(); + _gpuContext->consumeFrameUpdates(_activeFrame); } while (!pendingSize.empty()) { diff --git a/tools/gpu-frame-player/src/RenderThread.h b/tools/gpu-frame-player/src/RenderThread.h index 7312fece6c..09eef56623 100644 --- a/tools/gpu-frame-player/src/RenderThread.h +++ b/tools/gpu-frame-player/src/RenderThread.h @@ -55,6 +55,8 @@ public: std::queue _pendingSize; gpu::FramePointer _activeFrame; uint32_t _externalTexture{ 0 }; + void move(const glm::vec3& v); + glm::mat4 _correction; void resize(const QSize& newSize); From 2f92e10142558de3999906fc05f3423f98ecdc7b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:43:45 -0800 Subject: [PATCH 033/130] Support using prefering mobile compressed textures on desktop --- libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index e02f12819e..a8b5ec85e8 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -24,6 +24,10 @@ #include #include +static const QString FORCE_MOBILE_TEXTURES_STRING{ "HIFI_FORCE_MOBILE_TEXTURES" }; +static bool FORCE_MOBILE_TEXTURES = QProcessEnvironment::systemEnvironment().contains(FORCE_MOBILE_TEXTURES_STRING); + + using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; @@ -45,9 +49,10 @@ bool GL45Backend::supportedTextureFormat(const gpu::Element& format) { case gpu::Semantic::COMPRESSED_EAC_RED_SIGNED: case gpu::Semantic::COMPRESSED_EAC_XY: case gpu::Semantic::COMPRESSED_EAC_XY_SIGNED: - return false; + return FORCE_MOBILE_TEXTURES; + default: - return true; + return FORCE_MOBILE_TEXTURES ? !format.isCompressed() : true; } } From b8d8079590f0e50207c68f7827eb50fcc4e0cc2c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:44:18 -0800 Subject: [PATCH 034/130] Enable transforming a frame to use only relative paths --- tools/noramlizeFrame.py | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tools/noramlizeFrame.py diff --git a/tools/noramlizeFrame.py b/tools/noramlizeFrame.py new file mode 100644 index 0000000000..f1012f6428 --- /dev/null +++ b/tools/noramlizeFrame.py @@ -0,0 +1,62 @@ +import os +import json +import shutil +import sys + +def scriptRelative(*paths): + scriptdir = os.path.dirname(os.path.realpath(sys.argv[0])) + result = os.path.join(scriptdir, *paths) + result = os.path.realpath(result) + result = os.path.normcase(result) + return result + + + +class FrameProcessor: + def __init__(self, filename): + self.filename = filename + dir, name = os.path.split(self.filename) + self.dir = dir + self.ktxDir = os.path.join(self.dir, 'ktx') + os.makedirs(self.ktxDir, exist_ok=True) + self.resDir = scriptRelative("../interface/resources") + + if (name.endswith(".json")): + self.name = name[0:-5] + else: + self.name = name + self.filename = self.filename + '.json' + + with open(self.filename, 'r') as f: + self.json = json.load(f) + + + def processKtx(self, texture): + if texture is None: return + if not 'ktxFile' in texture: return + sourceKtx = texture['ktxFile'] + if sourceKtx.startswith(':'): + sourceKtx = sourceKtx[1:] + while sourceKtx.startswith('/'): + sourceKtx = sourceKtx[1:] + sourceKtx = os.path.join(self.resDir, sourceKtx) + sourceKtxDir, sourceKtxName = os.path.split(sourceKtx) + destKtx = os.path.join(self.ktxDir, sourceKtxName) + if not os.path.isfile(destKtx): + shutil.copy(sourceKtx, destKtx) + newValue = 'ktx/' + sourceKtxName + texture['ktxFile'] = newValue + + + def process(self): + for texture in self.json['textures']: + self.processKtx(texture) + + with open(self.filename, 'w') as f: + json.dump(self.json, f, indent=2) + +fp = FrameProcessor("D:/Frames/20190114_1629.json") +fp.process() + + +#C:\Users\bdavi\git\hifi\interface\resources\meshes \ No newline at end of file From a310eec41e87f8222ee69da50f49c9a32c73d246 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:41:49 -0800 Subject: [PATCH 035/130] Support relative paths in serialized frames --- libraries/gpu/src/gpu/FrameReader.cpp | 51 +++++++++++++++++++++------ 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index c63eaf51c3..6e39a38097 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -34,11 +34,26 @@ public: return filename; } + static std::string getBaseDir(const std::string& filename) { + std::string result; + if (0 == filename.find("assets:")) { + auto lastSlash = filename.rfind('/'); + result = filename.substr(0, lastSlash + 1); + } else { + std::string result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString(); + if (*result.rbegin() != '/') { + result += '/'; + } + } + return result; + } + Deserializer(const std::string& filename, uint32_t externalTexture, const TextureLoader& loader) : - basename(getBaseName(filename)), externalTexture(externalTexture), textureLoader(loader) {} + basename(getBaseName(filename)), basedir(getBaseDir(filename)), externalTexture(externalTexture), textureLoader(loader) { + } const std::string basename; - std::string basedir; + const std::string basedir; std::string binaryFile; const uint32_t externalTexture; TextureLoader textureLoader; @@ -302,6 +317,21 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) { return nullptr; } + std::string source; + readOptional(source, node, keys::source); + + std::string ktxFile; + readOptional(ktxFile, node, keys::ktxFile); + Element ktxTexelFormat, ktxMipFormat; + if (!ktxFile.empty()) { + if (QFileInfo(ktxFile.c_str()).isRelative()) { + ktxFile = basedir + ktxFile; + } + ktx::StoragePointer ktxStorage{ new storage::FileStorage(ktxFile.c_str()) }; + auto ktxObject = ktx::KTX::create(ktxStorage); + Texture::evalTextureFormat(ktxObject->getHeader(), ktxTexelFormat, ktxMipFormat); + } + TextureUsageType usageType = node[keys::usageType]; Texture::Type type = node[keys::type]; glm::u16vec4 dims; @@ -312,6 +342,9 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) { uint16 mips = node[keys::mips]; uint16 samples = node[keys::samples]; Element texelFormat = readElement(node[keys::texelFormat]); + if (!ktxFile.empty() && (ktxMipFormat.isCompressed() != texelFormat.isCompressed())) { + texelFormat = ktxMipFormat; + } Sampler sampler; readOptionalTransformed(sampler, node, keys::sampler, [](const json& node) { return readSampler(node); }); TexturePointer result; @@ -325,8 +358,6 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) { auto& texture = *result; readOptional(texture._source, node, keys::source); - std::string ktxFile; - readOptional(ktxFile, node, keys::ktxFile); if (!ktxFile.empty()) { if (QFileInfo(ktxFile.c_str()).isRelative()) { ktxFile = basedir + "/" + ktxFile; @@ -359,6 +390,7 @@ ShaderPointer Deserializer::readShader(const json& node) { // FIXME support procedural shaders Shader::Type type = node[keys::type]; + std::string name = node[keys::name]; uint32_t id = node[keys::id]; ShaderPointer result; switch (type) { @@ -374,6 +406,9 @@ ShaderPointer Deserializer::readShader(const json& node) { default: throw std::runtime_error("not implemented"); } + if (result->getSource().name != name) { + throw std::runtime_error("Bad name match"); + } return result; } @@ -747,12 +782,6 @@ StereoState readStereoState(const json& node) { FramePointer Deserializer::deserializeFrame() { { std::string filename{ basename + ".json" }; - if (0 == basename.find("assets:")) { - auto lastSlash = basename.rfind('/'); - basedir = basename.substr(0, lastSlash); - } else { - basedir = QFileInfo(basename.c_str()).absolutePath().toStdString(); - } storage::FileStorage mappedFile(filename.c_str()); frameNode = json::parse(std::string((const char*)mappedFile.data(), mappedFile.size())); } @@ -808,7 +837,7 @@ FramePointer Deserializer::deserializeFrame() { swapchains = readArray(frameNode, keys::swapchains, [this](const json& node) { return readSwapchain(node); }); - queries = readArray(frameNode, keys::queries, [this](const json& node) { return readQuery(node); }); + queries = readArray(frameNode, keys::queries, [](const json& node) { return readQuery(node); }); frame.framebuffer = framebuffers[frameNode[keys::framebuffer].get()]; frame.view = readMat4(frameNode[keys::view]); frame.pose = readMat4(frameNode[keys::pose]); From e32a3ba20d0dde5b28c9efda9c2495dd477b386e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 13:19:45 -0800 Subject: [PATCH 036/130] organize new avatar intersection --- interface/src/avatar/AvatarManager.cpp | 100 +++++++++++++------------ 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1eb87c16f0..376c4e7931 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -652,28 +652,25 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic PROFILE_RANGE(simulation_physics, __FUNCTION__); - float distance = (float)INT_MAX; // with FLT_MAX bullet rayTest does not return results + float bulletDistance = (float)INT_MAX; // with FLT_MAX bullet rayTest does not return results glm::vec3 rayDirection = glm::normalize(ray.direction); - std::vector physicsResults = _myAvatar->getCharacterController()->rayTest(glmToBullet(ray.origin), glmToBullet(rayDirection), distance, QVector()); + std::vector physicsResults = _myAvatar->getCharacterController()->rayTest(glmToBullet(ray.origin), glmToBullet(rayDirection), bulletDistance, QVector()); if (physicsResults.size() > 0) { glm::vec3 rayDirectionInv = { rayDirection.x != 0.0f ? 1.0f / rayDirection.x : INFINITY, rayDirection.y != 0.0f ? 1.0f / rayDirection.y : INFINITY, rayDirection.z != 0.0f ? 1.0f / rayDirection.z : INFINITY }; - MyCharacterController::RayAvatarResult rayAvatarResult; - AvatarPointer avatar = nullptr; - - BoxFace face = BoxFace::UNKNOWN_FACE; - glm::vec3 surfaceNormal; - QVariantMap extraInfo; - for (auto &hit : physicsResults) { auto avatarID = hit._intersectWithAvatar; if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatarID)) || (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatarID))) { continue; } - + + MyCharacterController::RayAvatarResult rayAvatarResult; + BoxFace face = BoxFace::UNKNOWN_FACE; + QVariantMap extraInfo; + AvatarPointer avatar = nullptr; if (_myAvatar->getSessionUUID() != avatarID) { auto avatarMap = getHashCopy(); AvatarHash::iterator itr = avatarMap.find(avatarID); @@ -683,44 +680,43 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic } else { avatar = _myAvatar; } + if (!hit._isBound) { rayAvatarResult = hit; } else if (avatar) { auto &multiSpheres = avatar->getMultiSphereShapes(); if (multiSpheres.size() > 0) { - std::vector boxHits; + MyCharacterController::RayAvatarResult boxHit; + boxHit._distance = FLT_MAX; + for (size_t i = 0; i < hit._boundJoints.size(); i++) { assert(hit._boundJoints[i] < multiSpheres.size()); auto &mSphere = multiSpheres[hit._boundJoints[i]]; if (mSphere.isValid()) { float boundDistance = FLT_MAX; - BoxFace face; - glm::vec3 surfaceNormal; + BoxFace boundFace = BoxFace::UNKNOWN_FACE; + glm::vec3 boundSurfaceNormal; auto &bbox = mSphere.getBoundingBox(); - if (bbox.findRayIntersection(ray.origin, rayDirection, rayDirectionInv, boundDistance, face, surfaceNormal)) { - MyCharacterController::RayAvatarResult boxHit; - boxHit._distance = boundDistance; - boxHit._intersect = true; - boxHit._intersectionNormal = surfaceNormal; - boxHit._intersectionPoint = ray.origin + boundDistance * rayDirection; - boxHit._intersectWithAvatar = avatarID; - boxHit._intersectWithJoint = mSphere.getJointIndex(); - boxHits.push_back(boxHit); + if (bbox.findRayIntersection(ray.origin, rayDirection, rayDirectionInv, boundDistance, boundFace, boundSurfaceNormal)) { + if (boundDistance < boxHit._distance) { + boxHit._intersect = true; + boxHit._intersectWithAvatar = avatarID; + boxHit._intersectWithJoint = mSphere.getJointIndex(); + boxHit._distance = boundDistance; + boxHit._intersectionPoint = ray.origin + boundDistance * rayDirection; + boxHit._intersectionNormal = boundSurfaceNormal; + face = boundFace; + } } } } - if (boxHits.size() > 0) { - if (boxHits.size() > 1) { - std::sort(boxHits.begin(), boxHits.end(), [](const MyCharacterController::RayAvatarResult& hitA, - const MyCharacterController::RayAvatarResult& hitB) { - return hitA._distance < hitB._distance; - }); - } - rayAvatarResult = boxHits[0]; + if (boxHit._distance < FLT_MAX) { + rayAvatarResult = boxHit; } } } - if (pickAgainstMesh) { + + if (rayAvatarResult._intersect && pickAgainstMesh) { glm::vec3 localRayOrigin = avatar->worldToJointPoint(ray.origin, rayAvatarResult._intersectWithJoint); glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayDirection, rayAvatarResult._intersectWithJoint); @@ -734,29 +730,35 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic auto defaultFrameRayPoint = jointPosition + jointOrientation * localRayPoint; auto defaultFrameRayDirection = defaultFrameRayPoint - defaultFrameRayOrigin; - if (avatar->getSkeletonModel()->findRayIntersectionAgainstSubMeshes(defaultFrameRayOrigin, defaultFrameRayDirection, distance, face, surfaceNormal, extraInfo, true, false)) { - auto newDistance = glm::length(vec3FromVariant(extraInfo["worldIntersectionPoint"]) - defaultFrameRayOrigin); - rayAvatarResult._distance = newDistance; - rayAvatarResult._intersectionPoint = ray.origin + newDistance * rayDirection; - rayAvatarResult._intersectionNormal = surfaceNormal; - extraInfo["worldIntersectionPoint"] = vec3toVariant(rayAvatarResult._intersectionPoint); - break; + float subMeshDistance = FLT_MAX; + BoxFace subMeshFace = BoxFace::UNKNOWN_FACE; + glm::vec3 subMeshSurfaceNormal; + QVariantMap subMeshExtraInfo; + if (avatar->getSkeletonModel()->findRayIntersectionAgainstSubMeshes(defaultFrameRayOrigin, defaultFrameRayDirection, subMeshDistance, subMeshFace, subMeshSurfaceNormal, subMeshExtraInfo, true, false)) { + rayAvatarResult._distance = subMeshDistance; + rayAvatarResult._intersectionPoint = ray.origin + subMeshDistance * rayDirection; + rayAvatarResult._intersectionNormal = subMeshSurfaceNormal; + face = subMeshFace; + extraInfo = subMeshExtraInfo; + } else { + rayAvatarResult._intersect = false; } - } else if (rayAvatarResult._intersect){ + } + + if (rayAvatarResult._intersect) { + result.intersects = true; + result.avatarID = rayAvatarResult._intersectWithAvatar; + result.distance = rayAvatarResult._distance; + result.face = face; + result.intersection = rayAvatarResult._intersectionPoint; + result.surfaceNormal = rayAvatarResult._intersectionNormal; + result.jointIndex = rayAvatarResult._intersectWithJoint; + result.extraInfo = extraInfo; break; } } - if (rayAvatarResult._intersect) { - result.intersects = true; - result.avatarID = rayAvatarResult._intersectWithAvatar; - result.distance = rayAvatarResult._distance; - result.surfaceNormal = rayAvatarResult._intersectionNormal; - result.jointIndex = rayAvatarResult._intersectWithJoint; - result.intersection = ray.origin + rayAvatarResult._distance * rayDirection; - result.extraInfo = extraInfo; - result.face = face; - } } + return result; } From 0e35458f9eeb3f00f71886657b93f46238030552 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 13:27:47 -0800 Subject: [PATCH 037/130] use precisionPicking in RayPick::getAvatarIntersection --- interface/src/raypick/RayPick.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 24ba4435e2..d476357bab 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -56,7 +56,8 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { } PickResultPointer RayPick::getAvatarIntersection(const PickRay& pick) { - RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs(), true); + bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get()->getForceCoarsePicking()); + RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs(), precisionPicking); if (avatarRes.intersects) { return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick, avatarRes.surfaceNormal, avatarRes.extraInfo); } else { From 93a91cdba2802a0b0f9fd6da063ac8bd0749e4eb Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 29 Jan 2019 15:40:45 -0800 Subject: [PATCH 038/130] webengine fileselector --- .../resources/qml/+webengine/Browser.qml | 275 ++++++++++++++++++ .../resources/qml/+webengine/InfoView.qml | 50 ++++ .../qml/+webengine/TabletBrowser.qml | 125 ++++++++ interface/resources/qml/Browser.qml | 69 +---- interface/resources/qml/InfoView.qml | 3 +- interface/resources/qml/TabletBrowser.qml | 97 +----- .../+webengine/FlickableWebViewCore.qml | 189 ++++++++++++ .../qml/controls/FlickableWebViewCore.qml | 141 +-------- .../resources/qml/controls/TabletWebView.qml | 1 - .../controlsUit/+webengine/BaseWebView.qml | 38 +++ .../resources/qml/controlsUit/BaseWebView.qml | 27 +- .../qml/controlsUit/ProxyWebView.qml | 30 ++ interface/resources/qml/controlsUit/qmldir | 1 + libraries/shared/src/shared/FileUtils.cpp | 4 + libraries/ui/src/ui/OffscreenQmlSurface.cpp | 5 +- 15 files changed, 730 insertions(+), 325 deletions(-) create mode 100644 interface/resources/qml/+webengine/Browser.qml create mode 100644 interface/resources/qml/+webengine/InfoView.qml create mode 100644 interface/resources/qml/+webengine/TabletBrowser.qml create mode 100644 interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml create mode 100644 interface/resources/qml/controlsUit/+webengine/BaseWebView.qml create mode 100644 interface/resources/qml/controlsUit/ProxyWebView.qml diff --git a/interface/resources/qml/+webengine/Browser.qml b/interface/resources/qml/+webengine/Browser.qml new file mode 100644 index 0000000000..52157bdf42 --- /dev/null +++ b/interface/resources/qml/+webengine/Browser.qml @@ -0,0 +1,275 @@ +import QtQuick 2.5 +import QtWebChannel 1.0 +import QtWebEngine 1.5 + +import controlsUit 1.0 +import stylesUit 1.0 +import "qrc:////qml//windows" + +ScrollingWindow { + id: root + HifiConstants { id: hifi } + //HifiStyles.HifiConstants { id: hifistyles } + title: "Browser" + resizable: true + destroyOnHidden: true + width: 800 + height: 600 + property variant permissionsBar: {'securityOrigin':'none','feature':'none'} + property alias url: webview.url + property alias webView: webview + + signal loadingChanged(int status) + + x: 100 + y: 100 + + Component.onCompleted: { + focus = true + shown = true + addressBar.text = webview.url + } + + function setProfile(profile) { + webview.profile = profile; + } + + function showPermissionsBar(){ + permissionsContainer.visible=true; + } + + function hidePermissionsBar(){ + permissionsContainer.visible=false; + } + + function allowPermissions(){ + webview.grantFeaturePermission(permissionsBar.securityOrigin, permissionsBar.feature, true); + hidePermissionsBar(); + } + + function setAutoAdd(auto) { + desktop.setAutoAdd(auto); + } + + Item { + id:item + width: pane.contentWidth + implicitHeight: pane.scrollHeight + + Row { + id: buttons + spacing: 4 + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.leftMargin: 8 + HiFiGlyphs { + id: back; + enabled: webview.canGoBack; + text: hifi.glyphs.backward + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goBack() } + } + + HiFiGlyphs { + id: forward; + enabled: webview.canGoForward; + text: hifi.glyphs.forward + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } + } + + HiFiGlyphs { + id: reload; + enabled: webview.canGoForward; + text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } + } + + } + + Item { + id: border + height: 48 + anchors.top: parent.top + anchors.topMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.left: buttons.right + anchors.leftMargin: 8 + + Item { + id: barIcon + width: parent.height + height: parent.height + Image { + source: webview.icon; + x: (parent.height - height) / 2 + y: (parent.width - width) / 2 + sourceSize: Qt.size(width, height); + verticalAlignment: Image.AlignVCenter; + horizontalAlignment: Image.AlignHCenter + } + } + + TextField { + id: addressBar + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.left: barIcon.right + anchors.leftMargin: 0 + anchors.verticalCenter: parent.verticalCenter + focus: true + colorScheme: hifi.colorSchemes.dark + placeholderText: "Enter URL" + Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i") + Keys.onPressed: { + switch(event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + if (text.indexOf("http") != 0) { + text = "http://" + text; + } + root.hidePermissionsBar(); + root.keyboardRaised = false; + webview.url = text; + break; + } + } + } + } + + Rectangle { + id:permissionsContainer + visible:false + color: "#000000" + width: parent.width + anchors.top: buttons.bottom + height:40 + z:100 + gradient: Gradient { + GradientStop { position: 0.0; color: "black" } + GradientStop { position: 1.0; color: "grey" } + } + + RalewayLight { + id: permissionsInfo + anchors.right:permissionsRow.left + anchors.rightMargin: 32 + anchors.topMargin:8 + anchors.top:parent.top + text: "This site wants to use your microphone/camera" + size: 18 + color: hifi.colors.white + } + + Row { + id: permissionsRow + spacing: 4 + anchors.top:parent.top + anchors.topMargin: 8 + anchors.right: parent.right + visible: true + z:101 + + Button { + id:allow + text: "Allow" + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 120 + enabled: true + onClicked: root.allowPermissions(); + z:101 + } + + Button { + id:block + text: "Block" + color: hifi.buttons.red + colorScheme: root.colorScheme + width: 120 + enabled: true + onClicked: root.hidePermissionsBar(); + z:101 + } + } + } + + WebView { + id: webview + url: "https://highfidelity.com/" + profile: FileTypeProfile; + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.Deferred + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + + anchors.top: buttons.bottom + anchors.topMargin: 8 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + onFeaturePermissionRequested: { + if (feature == 2) { // QWebEnginePage::MediaAudioCapture + grantFeaturePermission(securityOrigin, feature, true); + } else { + permissionsBar.securityOrigin = securityOrigin; + permissionsBar.feature = feature; + root.showPermissionsBar(); + } + } + + onLoadingChanged: { + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { + addressBar.text = loadRequest.url + } + root.loadingChanged(loadRequest.status); + } + + onWindowCloseRequested: { + root.destroy(); + } + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + desktop.initWebviewProfileHandlers(webview.profile); + } + } + + } // item + + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } +} // dialog diff --git a/interface/resources/qml/+webengine/InfoView.qml b/interface/resources/qml/+webengine/InfoView.qml new file mode 100644 index 0000000000..eb190c3c45 --- /dev/null +++ b/interface/resources/qml/+webengine/InfoView.qml @@ -0,0 +1,50 @@ +// +// InfoView.qml +// +// Created by Bradley Austin Davis on 27 Apr 2015 +// 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 +// + +import QtQuick 2.5 +import Hifi 1.0 as Hifi + +import controlsUit 1.0 +import "qrc:////qml//windows" as Windows + +Windows.ScrollingWindow { + id: root + width: 800 + height: 800 + resizable: true + + Hifi.InfoView { + id: infoView + width: pane.contentWidth + implicitHeight: pane.scrollHeight + + WebView { + id: webview + objectName: "WebView" + anchors.fill: parent + url: infoView.url + } + } + + Component.onCompleted: { + centerWindow(root); + } + + onVisibleChanged: { + if (visible) { + centerWindow(root); + } + } + + function centerWindow() { + desktop.centerOnVisible(root); + } + +} diff --git a/interface/resources/qml/+webengine/TabletBrowser.qml b/interface/resources/qml/+webengine/TabletBrowser.qml new file mode 100644 index 0000000000..720a904231 --- /dev/null +++ b/interface/resources/qml/+webengine/TabletBrowser.qml @@ -0,0 +1,125 @@ +import QtQuick 2.5 +import QtWebChannel 1.0 +import QtWebEngine 1.5 + +import "controls" +import controlsUit 1.0 as HifiControls +import "styles" as HifiStyles +import stylesUit 1.0 +import "windows" + +Item { + id: root + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } + + height: 600 + property variant permissionsBar: {'securityOrigin':'none','feature':'none'} + property alias url: webview.url + + property bool canGoBack: webview.canGoBack + property bool canGoForward: webview.canGoForward + + + signal loadingChanged(int status) + + x: 0 + y: 0 + + function setProfile(profile) { + webview.profile = profile; + } + + WebEngineView { + id: webview + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFWebEngineProfile; + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webview.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + + // Ensure the JS from the web-engine makes it to our logging + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + webview.profile.httpUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + web.address = url; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + urlAppend(loadRequest.url.toString()) + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onNewViewRequested: { + request.openIn(webView); + } + + HifiControls.WebSpinner { } + } + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } +} diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 4ddee15a10..78ffb51e67 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,16 +1,14 @@ import QtQuick 2.5 -import QtWebChannel 1.0 -import QtWebEngine 1.5 import controlsUit 1.0 -import "styles" as HifiStyles import stylesUit 1.0 + import "windows" ScrollingWindow { id: root HifiConstants { id: hifi } - HifiStyles.HifiConstants { id: hifistyles } + //HifiStyles.HifiConstants { id: hifistyles } title: "Browser" resizable: true destroyOnHidden: true @@ -32,7 +30,6 @@ ScrollingWindow { } function setProfile(profile) { - webview.profile = profile; } function showPermissionsBar(){ @@ -44,7 +41,6 @@ ScrollingWindow { } function allowPermissions(){ - webview.grantFeaturePermission(permissionsBar.securityOrigin, permissionsBar.feature, true); hidePermissionsBar(); } @@ -68,7 +64,7 @@ ScrollingWindow { id: back; enabled: webview.canGoBack; text: hifi.glyphs.backward - color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + color: enabled ? hifi.colors.text : hifi.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goBack() } } @@ -77,7 +73,7 @@ ScrollingWindow { id: forward; enabled: webview.canGoForward; text: hifi.glyphs.forward - color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + color: enabled ? hifi.colors.text : hifi.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } @@ -86,7 +82,7 @@ ScrollingWindow { id: reload; enabled: webview.canGoForward; text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload - color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + color: enabled ? hifi.colors.text : hifi.colors.disabledText size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } @@ -202,61 +198,10 @@ ScrollingWindow { } } - WebView { + ProxyWebView { id: webview + anchors.centerIn: parent url: "https://highfidelity.com/" - profile: FileTypeProfile; - - // Create a global EventBridge object for raiseAndLowerKeyboard. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.Deferred - worldId: WebEngineScript.MainWorld - } - - // Detect when may want to raise and lower keyboard. - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] - - anchors.top: buttons.bottom - anchors.topMargin: 8 - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - - onFeaturePermissionRequested: { - if (feature == 2) { // QWebEnginePage::MediaAudioCapture - grantFeaturePermission(securityOrigin, feature, true); - } else { - permissionsBar.securityOrigin = securityOrigin; - permissionsBar.feature = feature; - root.showPermissionsBar(); - } - } - - onLoadingChanged: { - if (loadRequest.status === WebEngineView.LoadSucceededStatus) { - addressBar.text = loadRequest.url - } - root.loadingChanged(loadRequest.status); - } - - onWindowCloseRequested: { - root.destroy(); - } - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - desktop.initWebviewProfileHandlers(webview.profile); - } } } // item diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 8c5900b4c3..5c2c7fcff9 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -19,13 +19,12 @@ Windows.ScrollingWindow { width: 800 height: 800 resizable: true - Hifi.InfoView { id: infoView width: pane.contentWidth implicitHeight: pane.scrollHeight - WebView { + ProxyWebView { id: webview objectName: "WebView" anchors.fill: parent diff --git a/interface/resources/qml/TabletBrowser.qml b/interface/resources/qml/TabletBrowser.qml index 720a904231..f83a9c81a5 100644 --- a/interface/resources/qml/TabletBrowser.qml +++ b/interface/resources/qml/TabletBrowser.qml @@ -1,17 +1,11 @@ import QtQuick 2.5 -import QtWebChannel 1.0 -import QtWebEngine 1.5 -import "controls" import controlsUit 1.0 as HifiControls -import "styles" as HifiStyles import stylesUit 1.0 -import "windows" Item { id: root HifiConstants { id: hifi } - HifiStyles.HifiConstants { id: hifistyles } height: 600 property variant permissionsBar: {'securityOrigin':'none','feature':'none'} @@ -30,96 +24,9 @@ Item { webview.profile = profile; } - WebEngineView { + HifiControls.ProxyWebView { id: webview - objectName: "webEngineView" - x: 0 - y: 0 width: parent.width - height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height - - profile: HFWebEngineProfile; - - property string userScriptUrl: "" - - // creates a global EventBridge object. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation - worldId: WebEngineScript.MainWorld - } - - // detects when to raise and lower virtual keyboard - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: webview.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] - - property string newUrl: "" - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - - // Ensure the JS from the web-engine makes it to our logging - webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); - }); - - webview.profile.httpUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; - web.address = url; - } - - onFeaturePermissionRequested: { - grantFeaturePermission(securityOrigin, feature, true); - } - - onLoadingChanged: { - keyboardRaised = false; - punctuationMode = false; - keyboard.resetShiftMode(false); - - // Required to support clicking on "hifi://" links - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - urlAppend(loadRequest.url.toString()) - var url = loadRequest.url.toString(); - if (urlHandler.canHandleUrl(url)) { - if (urlHandler.handleUrl(url)) { - root.stop(); - } - } - } - } - - onNewViewRequested: { - request.openIn(webView); - } - - HifiControls.WebSpinner { } - } - - Keys.onPressed: { - switch(event.key) { - case Qt.Key_L: - if (event.modifiers == Qt.ControlModifier) { - event.accepted = true - addressBar.selectAll() - addressBar.forceActiveFocus() - } - break; - } + height: parent.height } } diff --git a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml new file mode 100644 index 0000000000..823d0107a2 --- /dev/null +++ b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml @@ -0,0 +1,189 @@ +import QtQuick 2.7 +import QtWebEngine 1.5 +import QtWebChannel 1.0 + +import QtQuick.Controls 2.2 + +import stylesUit 1.0 as StylesUIt + +Item { + id: flick + + property alias url: webViewCore.url + property alias canGoBack: webViewCore.canGoBack + property alias webViewCore: webViewCore + property alias webViewCoreProfile: webViewCore.profile + property string webViewCoreUserAgent + + property string userScriptUrl: "" + property string urlTag: "noDownload=false"; + + signal newViewRequestedCallback(var request) + signal loadingChangedCallback(var loadRequest) + + + width: parent.width + + property bool interactive: false + + property bool blurOnCtrlShift: true + + StylesUIt.HifiConstants { + id: hifi + } + + function stop() { + webViewCore.stop(); + } + + Timer { + id: delayedUnfocuser + repeat: false + interval: 200 + onTriggered: { + + // The idea behind this is to delay unfocusing, so that fast lower/raise will not result actual unfocusing. + // Fast lower/raise happens every time keyboard is being re-raised (see the code below in OffscreenQmlSurface::setKeyboardRaised) + // + // if (raised) { + // item->setProperty("keyboardRaised", QVariant(!raised)); + // } + // + // item->setProperty("keyboardRaised", QVariant(raised)); + // + + webViewCore.runJavaScript("if (document.activeElement) document.activeElement.blur();", function(result) { + console.log('unfocus completed: ', result); + }); + } + } + + function unfocus() { + delayedUnfocuser.start(); + } + + function stopUnfocus() { + delayedUnfocuser.stop(); + } + + function onLoadingChanged(loadRequest) { + if (WebEngineView.LoadStartedStatus === loadRequest.status) { + + // Required to support clicking on "hifi://" links + var url = loadRequest.url.toString(); + url = (url.indexOf("?") >= 0) ? url + urlTag : url + "?" + urlTag; + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + webViewCore.stop(); + } + } + } + + if (WebEngineView.LoadFailedStatus === loadRequest.status) { + console.log("Tablet WebEngineView failed to load url: " + loadRequest.url.toString()); + } + + if (WebEngineView.LoadSucceededStatus === loadRequest.status) { + //disable Chromium's scroll bars + } + } + + WebEngineView { + id: webViewCore + + width: parent.width + height: parent.height + + profile: HFWebEngineProfile; + settings.pluginsEnabled: true + settings.touchIconsEnabled: true + settings.allowRunningInsecureContent: true + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: flick.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + + if (webViewCoreUserAgent !== undefined) { + webViewCore.profile.httpUserAgent = webViewCoreUserAgent + } else { + webViewCore.profile.httpUserAgent += " (HighFidelityInterface)"; + } + // Ensure the JS from the web-engine makes it to our logging + webViewCore.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + //disable popup + onContextMenuRequested: { + request.accepted = true; + } + + onNewViewRequested: { + newViewRequestedCallback(request) + } + + // Prior to 5.10, the WebEngineView loading property is true during initial page loading and then stays false + // as in-page javascript adds more html content. However, in 5.10 there is a bug such that adding html turns + // loading true, and never turns it false again. safeLoading provides a workaround, but it should be removed + // when QT fixes this. + property bool safeLoading: false + property bool loadingLatched: false + property var loadingRequest: null + onLoadingChanged: { + webViewCore.loadingRequest = loadRequest; + webViewCore.safeLoading = webViewCore.loading && !loadingLatched; + webViewCore.loadingLatched |= webViewCore.loading; + } + onSafeLoadingChanged: { + flick.onLoadingChanged(webViewCore.loadingRequest) + loadingChangedCallback(webViewCore.loadingRequest) + } + } + + AnimatedImage { + //anchoring doesnt works here when changing content size + x: flick.width/2 - width/2 + y: flick.height/2 - height/2 + source: "../../icons/loader-snake-64-w.gif" + visible: webViewCore.safeLoading && /^(http.*|)$/i.test(webViewCore.url.toString()) + playing: visible + z: 10000 + } + + Keys.onPressed: { + if (blurOnCtrlShift && (event.modifiers & Qt.ShiftModifier) && (event.modifiers & Qt.ControlModifier)) { + webViewCore.focus = false; + } + } +} diff --git a/interface/resources/qml/controls/FlickableWebViewCore.qml b/interface/resources/qml/controls/FlickableWebViewCore.qml index 823d0107a2..a844c8b624 100644 --- a/interface/resources/qml/controls/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/FlickableWebViewCore.qml @@ -1,10 +1,9 @@ import QtQuick 2.7 -import QtWebEngine 1.5 -import QtWebChannel 1.0 import QtQuick.Controls 2.2 import stylesUit 1.0 as StylesUIt +import controlsUit 1.0 as ControlsUit Item { id: flick @@ -33,157 +32,21 @@ Item { } function stop() { - webViewCore.stop(); - } - Timer { - id: delayedUnfocuser - repeat: false - interval: 200 - onTriggered: { - - // The idea behind this is to delay unfocusing, so that fast lower/raise will not result actual unfocusing. - // Fast lower/raise happens every time keyboard is being re-raised (see the code below in OffscreenQmlSurface::setKeyboardRaised) - // - // if (raised) { - // item->setProperty("keyboardRaised", QVariant(!raised)); - // } - // - // item->setProperty("keyboardRaised", QVariant(raised)); - // - - webViewCore.runJavaScript("if (document.activeElement) document.activeElement.blur();", function(result) { - console.log('unfocus completed: ', result); - }); - } } function unfocus() { - delayedUnfocuser.start(); } function stopUnfocus() { - delayedUnfocuser.stop(); } function onLoadingChanged(loadRequest) { - if (WebEngineView.LoadStartedStatus === loadRequest.status) { - - // Required to support clicking on "hifi://" links - var url = loadRequest.url.toString(); - url = (url.indexOf("?") >= 0) ? url + urlTag : url + "?" + urlTag; - if (urlHandler.canHandleUrl(url)) { - if (urlHandler.handleUrl(url)) { - webViewCore.stop(); - } - } - } - - if (WebEngineView.LoadFailedStatus === loadRequest.status) { - console.log("Tablet WebEngineView failed to load url: " + loadRequest.url.toString()); - } - - if (WebEngineView.LoadSucceededStatus === loadRequest.status) { - //disable Chromium's scroll bars - } } - WebEngineView { + ControlsUit.ProxyWebView { id: webViewCore - width: parent.width height: parent.height - - profile: HFWebEngineProfile; - settings.pluginsEnabled: true - settings.touchIconsEnabled: true - settings.allowRunningInsecureContent: true - - // creates a global EventBridge object. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation - worldId: WebEngineScript.MainWorld - } - - // detects when to raise and lower virtual keyboard - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: flick.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - - if (webViewCoreUserAgent !== undefined) { - webViewCore.profile.httpUserAgent = webViewCoreUserAgent - } else { - webViewCore.profile.httpUserAgent += " (HighFidelityInterface)"; - } - // Ensure the JS from the web-engine makes it to our logging - webViewCore.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); - }); - } - - onFeaturePermissionRequested: { - grantFeaturePermission(securityOrigin, feature, true); - } - - //disable popup - onContextMenuRequested: { - request.accepted = true; - } - - onNewViewRequested: { - newViewRequestedCallback(request) - } - - // Prior to 5.10, the WebEngineView loading property is true during initial page loading and then stays false - // as in-page javascript adds more html content. However, in 5.10 there is a bug such that adding html turns - // loading true, and never turns it false again. safeLoading provides a workaround, but it should be removed - // when QT fixes this. - property bool safeLoading: false - property bool loadingLatched: false - property var loadingRequest: null - onLoadingChanged: { - webViewCore.loadingRequest = loadRequest; - webViewCore.safeLoading = webViewCore.loading && !loadingLatched; - webViewCore.loadingLatched |= webViewCore.loading; - } - onSafeLoadingChanged: { - flick.onLoadingChanged(webViewCore.loadingRequest) - loadingChangedCallback(webViewCore.loadingRequest) - } - } - - AnimatedImage { - //anchoring doesnt works here when changing content size - x: flick.width/2 - width/2 - y: flick.height/2 - height/2 - source: "../../icons/loader-snake-64-w.gif" - visible: webViewCore.safeLoading && /^(http.*|)$/i.test(webViewCore.url.toString()) - playing: visible - z: 10000 - } - - Keys.onPressed: { - if (blurOnCtrlShift && (event.modifiers & Qt.ShiftModifier) && (event.modifiers & Qt.ControlModifier)) { - webViewCore.focus = false; - } } } diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index 3959dbf01b..9cbbd48a22 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -1,5 +1,4 @@ import QtQuick 2.7 -import QtWebEngine 1.5 import controlsUit 1.0 as HiFiControls import "../styles" as HifiStyles import stylesUit 1.0 diff --git a/interface/resources/qml/controlsUit/+webengine/BaseWebView.qml b/interface/resources/qml/controlsUit/+webengine/BaseWebView.qml new file mode 100644 index 0000000000..fdd9c12220 --- /dev/null +++ b/interface/resources/qml/controlsUit/+webengine/BaseWebView.qml @@ -0,0 +1,38 @@ +// +// WebView.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtWebEngine 1.5 + +WebEngineView { + id: root + + Component.onCompleted: { + console.log("Connecting JS messaging to Hifi Logging") + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + } + + onLoadingChanged: { + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + WebSpinner { } +} diff --git a/interface/resources/qml/controlsUit/BaseWebView.qml b/interface/resources/qml/controlsUit/BaseWebView.qml index fdd9c12220..52b2d1f3db 100644 --- a/interface/resources/qml/controlsUit/BaseWebView.qml +++ b/interface/resources/qml/controlsUit/BaseWebView.qml @@ -8,31 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.7 -import QtWebEngine 1.5 +import "." -WebEngineView { +ProxyWebView { id: root - - Component.onCompleted: { - console.log("Connecting JS messaging to Hifi Logging") - // Ensure the JS from the web-engine makes it to our logging - root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); - }); - } - - onLoadingChanged: { - // Required to support clicking on "hifi://" links - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var url = loadRequest.url.toString(); - if (urlHandler.canHandleUrl(url)) { - if (urlHandler.handleUrl(url)) { - root.stop(); - } - } - } - } - - WebSpinner { } } diff --git a/interface/resources/qml/controlsUit/ProxyWebView.qml b/interface/resources/qml/controlsUit/ProxyWebView.qml new file mode 100644 index 0000000000..adcc472831 --- /dev/null +++ b/interface/resources/qml/controlsUit/ProxyWebView.qml @@ -0,0 +1,30 @@ +import QtQuick 2.7 +import stylesUit 1.0 + +Rectangle { + HifiConstants { + id: hifi + } + + color: hifi.colors.darkGray + + signal onNewViewRequested(); + + property string url: ""; + property bool canGoBack: false + property bool canGoForward: false + property string icon: "" + property var profile: {} + + property bool safeLoading: false + property bool loadingLatched: false + property var loadingRequest: null + + + Text { + anchors.centerIn: parent + text: "This feature is not supported" + font.pixelSize: 32 + color: hifi.colors.white + } +} diff --git a/interface/resources/qml/controlsUit/qmldir b/interface/resources/qml/controlsUit/qmldir index d0577f5575..e1665df40e 100644 --- a/interface/resources/qml/controlsUit/qmldir +++ b/interface/resources/qml/controlsUit/qmldir @@ -29,6 +29,7 @@ TextEdit 1.0 TextEdit.qml TextField 1.0 TextField.qml ToolTip 1.0 ToolTip.qml Tree 1.0 Tree.qml +ProxyWebView 1.0 ProxyWebView.qml VerticalSpacer 1.0 VerticalSpacer.qml WebGlyphButton 1.0 WebGlyphButton.qml WebSpinner 1.0 WebSpinner.qml diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 0709a53602..f65acccfa1 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -33,6 +33,10 @@ const QStringList& FileUtils::getFileSelectors() { #if defined(USE_GLES) extraSelectors << "gles"; #endif + +#ifndef Q_OS_ANDROID + extraSelectors << "webengine"; +#endif }); return extraSelectors; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index f67a356078..71bb65509f 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -49,6 +49,7 @@ #include #include "SecurityImageProvider.h" +#include "shared/FileUtils.h" #include "types/FileTypeProfile.h" #include "types/HFWebEngineProfile.h" #include "types/SoundEffect.h" @@ -237,7 +238,9 @@ void OffscreenQmlSurface::clearFocusItem() { void OffscreenQmlSurface::initializeEngine(QQmlEngine* engine) { Parent::initializeEngine(engine); - new QQmlFileSelector(engine); + QQmlFileSelector* fileSelector = new QQmlFileSelector(engine); + fileSelector->setExtraSelectors(FileUtils::getFileSelectors()); + static std::once_flag once; std::call_once(once, [] { qRegisterMetaType(); From 0846eb8ec68730af06629a6cea7ab7cffb39d86c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 30 Jan 2019 13:59:37 -0800 Subject: [PATCH 039/130] attempt to allow position edits in releaseGrab entity-method --- interface/src/avatar/MyAvatar.cpp | 10 ++++ .../src/avatars-renderer/Avatar.cpp | 3 ++ libraries/physics/src/EntityMotionState.cpp | 49 ++++++++++++++----- libraries/shared/src/Grab.h | 14 ++++-- libraries/shared/src/SpatiallyNestable.cpp | 7 ++- libraries/shared/src/SpatiallyNestable.h | 2 +- 6 files changed, 68 insertions(+), 17 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 92d9270d20..a0dd817742 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5300,6 +5300,16 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { bool tellHandler { false }; _avatarGrabsLock.withWriteLock([&] { + + std::map::iterator itr; + itr = _avatarGrabs.find(grabID); + if (itr != _avatarGrabs.end()) { + GrabPointer grab = itr->second; + if (grab) { + grab->setDeleted(true); + } + } + if (_avatarGrabData.remove(grabID)) { _grabsToDelete.push_back(grabID); tellHandler = true; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 07c1ca9a32..4bfaea0617 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -412,6 +412,9 @@ void Avatar::accumulateGrabPositions(std::map& g if (!grab || !grab->getActionID().isNull()) { continue; // the accumulated value isn't used, in this case. } + if (grab->getDeleted()) { + continue; + } glm::vec3 jointTranslation = getAbsoluteJointTranslationInObjectFrame(grab->getParentJointIndex()); glm::quat jointRotation = getAbsoluteJointRotationInObjectFrame(grab->getParentJointIndex()); diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index a82931064a..5a658e1f01 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -114,18 +114,45 @@ void EntityMotionState::updateServerPhysicsVariables() { } } +// void EntityMotionState::handleDeactivation() { +// // copy _server data to entity +// Transform localTransform = _entity->getLocalTransform(); +// // localTransform.setTranslation(_serverPosition); +// // localTransform.setRotation(_serverRotation); +// _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); +// // and also to RigidBody +// btTransform worldTrans; +// worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); +// worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); +// _body->setWorldTransform(worldTrans); +// // no need to update velocities... should already be zero +// } + void EntityMotionState::handleDeactivation() { - // copy _server data to entity - Transform localTransform = _entity->getLocalTransform(); - localTransform.setTranslation(_serverPosition); - localTransform.setRotation(_serverRotation); - _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); - worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero + if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { + // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or velocities + // at the last minute before deactivation --> the values stored in _server* and _body are stale. + // We assume the EntityMotionState is the last to know, so we copy from EntityItem and let things sort themselves out. + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); + _serverAcceleration = _entity->getAcceleration(); + _serverActionData = _entity->getDynamicData(); + _lastStep = ObjectMotionState::getWorldSimulationStep(); + } else { + // copy _server data to entity + Transform localTransform = _entity->getLocalTransform(); + localTransform.setTranslation(_serverPosition); + localTransform.setRotation(_serverRotation); + _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); + worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero + } } // virtual diff --git a/libraries/shared/src/Grab.h b/libraries/shared/src/Grab.h index 5765d6fd0e..59602439f7 100644 --- a/libraries/shared/src/Grab.h +++ b/libraries/shared/src/Grab.h @@ -26,16 +26,16 @@ public: void accumulate(glm::vec3 position, glm::quat orientation) { _position += position; _orientation = orientation; // XXX - count++; + _count++; } - glm::vec3 finalizePosition() { return count > 0 ? _position * (1.0f / count) : glm::vec3(0.0f); } + glm::vec3 finalizePosition() { return _count > 0 ? _position * (1.0f / _count) : glm::vec3(0.0f); } glm::quat finalizeOrientation() { return _orientation; } // XXX protected: glm::vec3 _position; glm::quat _orientation; - int count { 0 }; + int _count { 0 }; }; class Grab { @@ -48,7 +48,8 @@ public: _parentJointIndex(newParentJointIndex), _hand(newHand), _positionalOffset(newPositionalOffset), - _rotationalOffset(newRotationalOffset) {} + _rotationalOffset(newRotationalOffset), + _deleted(false) {} QByteArray toByteArray(); bool fromByteArray(const QByteArray& grabData); @@ -61,6 +62,7 @@ public: _positionalOffset = other->_positionalOffset; _rotationalOffset = other->_rotationalOffset; _actionID = other->_actionID; + _deleted = other->_deleted; return *this; } @@ -85,6 +87,9 @@ public: glm::quat getRotationalOffset() const { return _rotationalOffset; } void setRotationalOffset(glm::quat rotationalOffset) { _rotationalOffset = rotationalOffset; } + bool getDeleted() const { return _deleted; } + void setDeleted(bool value) { _deleted = value; } + protected: QUuid _actionID; // if an action is created in bullet for this grab, this is the ID QUuid _ownerID; // avatar ID of grabber @@ -93,6 +98,7 @@ protected: QString _hand; // "left" or "right" glm::vec3 _positionalOffset; // relative to joint glm::quat _rotationalOffset; // relative to joint + bool _deleted { false }; // scheduled for deletion }; diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index c524e3183b..dd0d749919 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -1390,7 +1390,12 @@ void SpatiallyNestable::removeGrab(GrabPointer grab) { bool SpatiallyNestable::hasGrabs() { bool result { false }; _grabsLock.withReadLock([&] { - result = !_grabs.isEmpty(); + foreach (const GrabPointer &grab, _grabs) { + if (grab && !grab->getDeleted()) { + result = true; + break; + } + } }); return result; } diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 319f07236b..ed432647fd 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -241,7 +241,7 @@ protected: quint64 _rotationChanged { 0 }; mutable ReadWriteLockable _grabsLock; - QSet _grabs; + QSet _grabs; // upon this thing private: SpatiallyNestable() = delete; From 3ab2db96b6ecd0d43d15dad43143f81a52c93218 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 30 Jan 2019 14:42:47 -0800 Subject: [PATCH 040/130] deactivate grab action when grab is released --- interface/src/avatar/MyAvatar.cpp | 5 ++++- .../avatars-renderer/src/avatars-renderer/Avatar.cpp | 2 +- libraries/entities/src/EntityDynamicInterface.h | 1 + libraries/entities/src/EntityItem.cpp | 10 ++++++++++ libraries/entities/src/EntityItem.h | 1 + libraries/shared/src/Grab.h | 10 +++++----- libraries/shared/src/SpatiallyNestable.cpp | 2 +- libraries/shared/src/SpatiallyNestable.h | 1 + 8 files changed, 24 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a0dd817742..0530ba8eb2 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5306,7 +5306,10 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { if (itr != _avatarGrabs.end()) { GrabPointer grab = itr->second; if (grab) { - grab->setDeleted(true); + grab->setReleased(true); + bool success; + SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success); + target->disableGrab(grab); } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 4bfaea0617..b8626c813e 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -412,7 +412,7 @@ void Avatar::accumulateGrabPositions(std::map& g if (!grab || !grab->getActionID().isNull()) { continue; // the accumulated value isn't used, in this case. } - if (grab->getDeleted()) { + if (grab->getReleased()) { continue; } diff --git a/libraries/entities/src/EntityDynamicInterface.h b/libraries/entities/src/EntityDynamicInterface.h index 836dae2057..c911eda471 100644 --- a/libraries/entities/src/EntityDynamicInterface.h +++ b/libraries/entities/src/EntityDynamicInterface.h @@ -59,6 +59,7 @@ public: virtual bool isReadyForAdd() const { return true; } bool isActive() { return _active; } + void deactivate() { _active = false; } virtual void removeFromSimulation(EntitySimulationPointer simulation) const = 0; virtual EntityItemWeakPointer getOwnerEntity() const = 0; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 41e4f43a5d..1640e97ff4 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -3506,3 +3506,13 @@ void EntityItem::removeGrab(GrabPointer grab) { } disableNoBootstrap(); } + +void EntityItem::disableGrab(GrabPointer grab) { + QUuid actionID = grab->getActionID(); + if (!actionID.isNull()) { + EntityDynamicPointer action = _grabActions.value(actionID); + if (action) { + action->deactivate(); + } + } +} diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index ec7ad78313..27b207b6f3 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -561,6 +561,7 @@ public: virtual void addGrab(GrabPointer grab) override; virtual void removeGrab(GrabPointer grab) override; + virtual void disableGrab(GrabPointer grab) override; signals: void requestRenderUpdate(); diff --git a/libraries/shared/src/Grab.h b/libraries/shared/src/Grab.h index 59602439f7..f16a80befa 100644 --- a/libraries/shared/src/Grab.h +++ b/libraries/shared/src/Grab.h @@ -49,7 +49,7 @@ public: _hand(newHand), _positionalOffset(newPositionalOffset), _rotationalOffset(newRotationalOffset), - _deleted(false) {} + _released(false) {} QByteArray toByteArray(); bool fromByteArray(const QByteArray& grabData); @@ -62,7 +62,7 @@ public: _positionalOffset = other->_positionalOffset; _rotationalOffset = other->_rotationalOffset; _actionID = other->_actionID; - _deleted = other->_deleted; + _released = other->_released; return *this; } @@ -87,8 +87,8 @@ public: glm::quat getRotationalOffset() const { return _rotationalOffset; } void setRotationalOffset(glm::quat rotationalOffset) { _rotationalOffset = rotationalOffset; } - bool getDeleted() const { return _deleted; } - void setDeleted(bool value) { _deleted = value; } + bool getReleased() const { return _released; } + void setReleased(bool value) { _released = value; } protected: QUuid _actionID; // if an action is created in bullet for this grab, this is the ID @@ -98,7 +98,7 @@ protected: QString _hand; // "left" or "right" glm::vec3 _positionalOffset; // relative to joint glm::quat _rotationalOffset; // relative to joint - bool _deleted { false }; // scheduled for deletion + bool _released { false }; // released and scheduled for deletion }; diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index dd0d749919..d3ed79faf4 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -1391,7 +1391,7 @@ bool SpatiallyNestable::hasGrabs() { bool result { false }; _grabsLock.withReadLock([&] { foreach (const GrabPointer &grab, _grabs) { - if (grab && !grab->getDeleted()) { + if (grab && !grab->getReleased()) { result = true; break; } diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index ed432647fd..e7a449f73f 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -218,6 +218,7 @@ public: virtual void addGrab(GrabPointer grab); virtual void removeGrab(GrabPointer grab); + virtual void disableGrab(GrabPointer grab) {}; bool hasGrabs(); virtual QUuid getEditSenderID(); From f33e5ba5ae5d0df55018128489b4c115ef6b8111 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 30 Jan 2019 14:47:38 -0800 Subject: [PATCH 041/130] cleanup --- libraries/physics/src/EntityMotionState.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 5a658e1f01..ce9cb20c21 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -114,20 +114,6 @@ void EntityMotionState::updateServerPhysicsVariables() { } } -// void EntityMotionState::handleDeactivation() { -// // copy _server data to entity -// Transform localTransform = _entity->getLocalTransform(); -// // localTransform.setTranslation(_serverPosition); -// // localTransform.setRotation(_serverRotation); -// _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); -// // and also to RigidBody -// btTransform worldTrans; -// worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); -// worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); -// _body->setWorldTransform(worldTrans); -// // no need to update velocities... should already be zero -// } - void EntityMotionState::handleDeactivation() { if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or velocities From c6f44234f8afdc1f30f5c4ac2b1b317c8027ef75 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 30 Jan 2019 14:56:02 -0800 Subject: [PATCH 042/130] avoid possible crash --- interface/src/avatar/MyAvatar.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0530ba8eb2..2eae7aa181 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5309,7 +5309,9 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { grab->setReleased(true); bool success; SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success); - target->disableGrab(grab); + if (target) { + target->disableGrab(grab); + } } } From 30d9fe705ebee66d6d30a3233959a699c3ad0010 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 30 Jan 2019 14:56:50 -0800 Subject: [PATCH 043/130] avoid possible crash --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2eae7aa181..3f32f96795 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5309,7 +5309,7 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { grab->setReleased(true); bool success; SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success); - if (target) { + if (target && success) { target->disableGrab(grab); } } From 03789e01da412cc50cb8ecb86d1a5f03833b0509 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Wed, 30 Jan 2019 15:53:21 -0800 Subject: [PATCH 044/130] adding fix for oculus store argument check --- interface/src/Application.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index daf2dd6363..3b9fe15f25 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -761,6 +761,11 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store + static const auto OCULUS_STORE_ARG = "--oculus-store"; + bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); + qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); + // Ignore any previous crashes if running from command line with a test script. bool inTestMode { false }; for (int i = 0; i < argc; ++i) { @@ -1138,10 +1143,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; #endif - // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store - static const QString OCULUS_STORE_ARG = "--oculus-store"; - bool isStore = arguments().indexOf(OCULUS_STORE_ARG) != -1; - setProperty(hifi::properties::OCULUS_STORE, isStore); + bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); + DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? updateHeartbeat(); From 6cbc0fad7f4304a8189af1f4e8d37de8f96dac1a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 15:33:14 -0800 Subject: [PATCH 045/130] fixes --- interface/src/avatar/AvatarManager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 376c4e7931..0b33220c01 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -718,7 +718,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic if (rayAvatarResult._intersect && pickAgainstMesh) { glm::vec3 localRayOrigin = avatar->worldToJointPoint(ray.origin, rayAvatarResult._intersectWithJoint); - glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayDirection, rayAvatarResult._intersectWithJoint); + glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayAvatarResult._distance * rayDirection, rayAvatarResult._intersectWithJoint); auto avatarOrientation = avatar->getWorldOrientation(); auto avatarPosition = avatar->getWorldPosition(); @@ -728,7 +728,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic auto defaultFrameRayOrigin = jointPosition + jointOrientation * localRayOrigin; auto defaultFrameRayPoint = jointPosition + jointOrientation * localRayPoint; - auto defaultFrameRayDirection = defaultFrameRayPoint - defaultFrameRayOrigin; + auto defaultFrameRayDirection = glm::normalize(defaultFrameRayPoint - defaultFrameRayOrigin); float subMeshDistance = FLT_MAX; BoxFace subMeshFace = BoxFace::UNKNOWN_FACE; @@ -750,7 +750,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic result.avatarID = rayAvatarResult._intersectWithAvatar; result.distance = rayAvatarResult._distance; result.face = face; - result.intersection = rayAvatarResult._intersectionPoint; + result.intersection = ray.origin + rayAvatarResult._distance * rayDirection; result.surfaceNormal = rayAvatarResult._intersectionNormal; result.jointIndex = rayAvatarResult._intersectWithJoint; result.extraInfo = extraInfo; From 8a1a55189a514fddb4e7e4b248eb22ddbe6a9305 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:37:56 -0800 Subject: [PATCH 046/130] CMake cleanup and modernization --- cmake/macros/IncludeHifiLibraryHeaders.cmake | 2 +- cmake/macros/LinkHifiLibraries.cmake | 4 ++-- interface/CMakeLists.txt | 2 +- libraries/entities/CMakeLists.txt | 2 +- libraries/gl/CMakeLists.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmake/macros/IncludeHifiLibraryHeaders.cmake b/cmake/macros/IncludeHifiLibraryHeaders.cmake index 913d1e1181..008d76a8dc 100644 --- a/cmake/macros/IncludeHifiLibraryHeaders.cmake +++ b/cmake/macros/IncludeHifiLibraryHeaders.cmake @@ -10,5 +10,5 @@ # macro(include_hifi_library_headers LIBRARY) - include_directories("${HIFI_LIBRARY_DIR}/${LIBRARY}/src") + target_include_directories(${TARGET_NAME} PRIVATE "${HIFI_LIBRARY_DIR}/${LIBRARY}/src") endmacro(include_hifi_library_headers _library _root_dir) \ No newline at end of file diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index 7a6a136799..6a430f5b13 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -19,8 +19,8 @@ function(LINK_HIFI_LIBRARIES) endforeach() foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK}) - include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") - include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}") + target_include_directories(${TARGET_NAME} PRIVATE "${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") + target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}") # link the actual library - it is static so don't bubble it up target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY}) endforeach() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index c013cfacd3..81b9935aa5 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -265,7 +265,7 @@ foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) endforeach() # include headers for interface and InterfaceConfig. -include_directories("${PROJECT_SOURCE_DIR}/src") +target_include_directories(${TARGET_NAME} PRIVATE "${PROJECT_SOURCE_DIR}/src") if (ANDROID) find_library(ANDROID_LOG_LIB log) diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index fcbe563f88..e359c7132f 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME entities) setup_hifi_library(Network Script) -include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") +target_include_directories(${TARGET_NAME} PRIVATE "${OPENSSL_INCLUDE_DIR}") include_hifi_library_headers(hfm) include_hifi_library_headers(fbx) include_hifi_library_headers(gpu) diff --git a/libraries/gl/CMakeLists.txt b/libraries/gl/CMakeLists.txt index 925cf9b288..855452e8f0 100644 --- a/libraries/gl/CMakeLists.txt +++ b/libraries/gl/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME gl) -setup_hifi_library(Gui Widgets Qml Quick) +setup_hifi_library(Gui Widgets) link_hifi_libraries(shared) target_opengl() From c3c22aa84c759a8308aee98e7790a5ffab58af9b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:37:24 -0800 Subject: [PATCH 047/130] EGL and Oculus depedency macros --- cmake/macros/TargetEGL.cmake | 4 ++++ cmake/macros/TargetOculusMobile.cmake | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 cmake/macros/TargetEGL.cmake create mode 100644 cmake/macros/TargetOculusMobile.cmake diff --git a/cmake/macros/TargetEGL.cmake b/cmake/macros/TargetEGL.cmake new file mode 100644 index 0000000000..1d8ce26d83 --- /dev/null +++ b/cmake/macros/TargetEGL.cmake @@ -0,0 +1,4 @@ +macro(target_egl) + find_library(EGL EGL) + target_link_libraries(${TARGET_NAME} ${EGL}) +endmacro() diff --git a/cmake/macros/TargetOculusMobile.cmake b/cmake/macros/TargetOculusMobile.cmake new file mode 100644 index 0000000000..3eaa008b14 --- /dev/null +++ b/cmake/macros/TargetOculusMobile.cmake @@ -0,0 +1,20 @@ + +macro(target_oculus_mobile) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus/VrApi) + + # Mobile SDK + set(OVR_MOBILE_INCLUDE_DIRS ${INSTALL_DIR}/Include) + target_include_directories(${TARGET_NAME} PRIVATE ${OVR_MOBILE_INCLUDE_DIRS}) + set(OVR_MOBILE_LIBRARY_DIR ${INSTALL_DIR}/Libs/Android/arm64-v8a) + set(OVR_MOBILE_LIBRARY_RELEASE ${OVR_MOBILE_LIBRARY_DIR}/Release/libvrapi.so) + set(OVR_MOBILE_LIBRARY_DEBUG ${OVR_MOBILE_LIBRARY_DIR}/Debug/libvrapi.so) + select_library_configurations(OVR_MOBILE) + target_link_libraries(${TARGET_NAME} ${OVR_MOBILE_LIBRARIES}) + + # Platform SDK + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculusPlatform) + set(OVR_PLATFORM_INCLUDE_DIRS ${INSTALL_DIR}/Include) + target_include_directories(${TARGET_NAME} PRIVATE ${OVR_PLATFORM_INCLUDE_DIRS}) + set(OVR_PLATFORM_LIBRARIES ${INSTALL_DIR}/Android/libs/arm64-v8a/libovrplatformloader.so) + target_link_libraries(${TARGET_NAME} ${OVR_PLATFORM_LIBRARIES}) +endmacro() From fed9e27a66e5f3842cfb010e0a59b3d038cd568a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:38:24 -0800 Subject: [PATCH 048/130] Expose current android app name to source code --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6956fd22c3..4d616e1f3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ endif() if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) + add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") else () set(PLATFORM_QT_COMPONENTS WebEngine) endif () From 5d1277e1bbe2e1ecf31289a88d9ce324d5ee33fc Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:42:33 -0800 Subject: [PATCH 049/130] GL helpers cleanup --- libraries/gl/src/gl/Context.cpp | 21 +++++++++++++- libraries/gl/src/gl/ContextQt.cpp | 7 +++-- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 12 -------- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 29 ++++++++++--------- libraries/gl/src/gl/QOpenGLContextWrapper.h | 22 +++++++++++--- 5 files changed, 59 insertions(+), 32 deletions(-) diff --git a/libraries/gl/src/gl/Context.cpp b/libraries/gl/src/gl/Context.cpp index 7d27b42909..a0d52ee223 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -195,6 +195,21 @@ GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB; Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); +#if defined(GL_CUSTOM_CONTEXT) +bool Context::makeCurrent() { + BOOL result = wglMakeCurrent(_hdc, _hglrc); + assert(result); + updateSwapchainMemoryCounter(); + return result; +} + void Context::swapBuffers() { + SwapBuffers(_hdc); +} + void Context::doneCurrent() { + wglMakeCurrent(0, 0); +} +#endif + void Context::create(QOpenGLContext* shareContext) { if (!shareContext) { shareContext = qt_gl_global_share_context(); @@ -297,7 +312,11 @@ void Context::create(QOpenGLContext* shareContext) { contextAttribs.push_back(0); } contextAttribs.push_back(0); - HGLRC shareHglrc = (HGLRC)QOpenGLContextWrapper::nativeContext(shareContext); + HGLRC shareHglrc = nullptr; + if (shareContext) { + auto nativeContextPointer = QOpenGLContextWrapper(shareContext).getNativeContext(); + shareHglrc = (HGLRC)nativeContextPointer->context(); + } _hglrc = wglCreateContextAttribsARB(_hdc, shareHglrc, &contextAttribs[0]); } diff --git a/libraries/gl/src/gl/ContextQt.cpp b/libraries/gl/src/gl/ContextQt.cpp index 82724dfd62..24ae29e4ca 100644 --- a/libraries/gl/src/gl/ContextQt.cpp +++ b/libraries/gl/src/gl/ContextQt.cpp @@ -61,12 +61,12 @@ void Context::debugMessageHandler(const QOpenGLDebugMessage& debugMessage) { switch (severity) { case QOpenGLDebugMessage::NotificationSeverity: case QOpenGLDebugMessage::LowSeverity: + qCDebug(glLogging) << debugMessage; return; default: + qCWarning(glLogging) << debugMessage; break; } - qWarning(glLogging) << debugMessage; - return; } void Context::setupDebugLogging(QOpenGLContext *context) { @@ -82,6 +82,8 @@ void Context::setupDebugLogging(QOpenGLContext *context) { } } + +#if !defined(GL_CUSTOM_CONTEXT) bool Context::makeCurrent() { updateSwapchainMemoryCounter(); bool result = _qglContext->makeCurrent(_window); @@ -98,6 +100,7 @@ void Context::doneCurrent() { _qglContext->doneCurrent(); } } +#endif Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index f05acb50e9..69b41da821 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -65,21 +65,9 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { _offscreenSurface->setFormat(_context->format()); _offscreenSurface->create(); - - // Due to a https://bugreports.qt.io/browse/QTBUG-65125 we can't rely on `isValid` - // to determine if the offscreen surface was successfully created, so we use - // makeCurrent as a proxy test. Bug is fixed in Qt 5.9.4 -#if defined(Q_OS_ANDROID) - if (!_context->makeCurrent(_offscreenSurface)) { - qFatal("Unable to make offscreen surface current"); - } - _context->doneCurrent(); -#else if (!_offscreenSurface->isValid()) { qFatal("Offscreen surface is invalid"); } -#endif - return true; } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index fbebb1128d..842c7abd08 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -17,6 +17,22 @@ #include #endif +QOpenGLContextWrapper::Pointer QOpenGLContextWrapper::currentContextWrapper() { + return std::make_shared(QOpenGLContext::currentContext()); +} + + +QOpenGLContextWrapper::NativeContextPointer QOpenGLContextWrapper::getNativeContext() const { + QOpenGLContextWrapper::NativeContextPointer result; + auto nativeHandle = _context->nativeHandle(); + if (nativeHandle.canConvert()) { + result = std::make_shared(); + *result = nativeHandle.value(); + } + return result; +} + + uint32_t QOpenGLContextWrapper::currentContextVersion() { QOpenGLContext* context = QOpenGLContext::currentContext(); if (!context) { @@ -49,19 +65,6 @@ void QOpenGLContextWrapper::setFormat(const QSurfaceFormat& format) { _context->setFormat(format); } -#ifdef Q_OS_WIN -void* QOpenGLContextWrapper::nativeContext(QOpenGLContext* context) { - HGLRC result = 0; - if (context != nullptr) { - auto nativeHandle = context->nativeHandle(); - if (nativeHandle.canConvert()) { - result = nativeHandle.value().context(); - } - } - return result; -} -#endif - bool QOpenGLContextWrapper::create() { return _context->create(); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 32ba7f22e8..1fade3e7fa 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -12,19 +12,31 @@ #ifndef hifi_QOpenGLContextWrapper_h #define hifi_QOpenGLContextWrapper_h -#include #include +#include class QOpenGLContext; class QSurface; class QSurfaceFormat; class QThread; +#if defined(Q_OS_ANDROID) +#include +#include +using QGLNativeContext = QEGLNativeContext; +#elif defined(Q_OS_WIN) +class QWGLNativeContext; +using QGLNativeContext = QWGLNativeContext; +#else +using QGLNativeContext = void*; +#endif + class QOpenGLContextWrapper { public: -#ifdef Q_OS_WIN - static void* nativeContext(QOpenGLContext* context); -#endif + using Pointer = std::shared_ptr; + using NativeContextPointer = std::shared_ptr; + static Pointer currentContextWrapper(); + QOpenGLContextWrapper(); QOpenGLContextWrapper(QOpenGLContext* context); @@ -37,6 +49,8 @@ public: void setShareContext(QOpenGLContext* otherContext); void moveToThread(QThread* thread); + NativeContextPointer getNativeContext() const; + static QOpenGLContext* currentContext(); static uint32_t currentContextVersion(); From b1eb0b0a46fabdea7ca3122a64253c7265b95095 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 13:10:52 -0800 Subject: [PATCH 050/130] GPU tweaks --- .../gpu-gl-common/src/gpu/gl/GLBackend.cpp | 3 + .../gpu-gl-common/src/gpu/gl/GLBackend.h | 2 +- libraries/gpu-gles/src/gpu/gles/GLESBackend.h | 1 + .../src/gpu/gles/GLESBackendOutput.cpp | 66 ++++++++++++++++++- libraries/gpu/src/gpu/Context.h | 3 +- 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp index 82f4f97e3b..1cf331cd1a 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp @@ -426,6 +426,9 @@ void GLBackend::render(const Batch& batch) { GL_PROFILE_RANGE(render_gpu_gl, batch.getName().c_str()); _transform._skybox = _stereo._skybox = batch.isSkyboxEnabled(); + // FIXME move this to between the transfer and draw passes, so that + // framebuffer setup can see the proper stereo state and enable things + // like foveation // Allow the batch to override the rendering stereo settings // for things like full framebuffer copy operations (deferred lighting passes) bool savedStereo = _stereo._enable; diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h index b5a279a54c..671d4e11d7 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h @@ -95,7 +95,7 @@ public: // Shutdown rendering and persist any required resources void shutdown() override; - void setCameraCorrection(const Mat4& correction, const Mat4& prevRenderView, bool reset = false); + void setCameraCorrection(const Mat4& correction, const Mat4& prevRenderView, bool reset = false) override; void render(const Batch& batch) final override; // This call synchronize the Full Backend cache with the current GLState diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h index aaa1be5892..636518c85a 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h @@ -48,6 +48,7 @@ public: class GLESTexture : public GLTexture { using Parent = GLTexture; friend class GLESBackend; + friend class GLESFramebuffer; GLuint allocate(const Texture& texture); protected: GLESTexture(const std::weak_ptr& backend, const Texture& buffer); diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp index 9c3a83ce13..90ce8c853a 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp @@ -17,6 +17,34 @@ namespace gpu { namespace gles { + +// returns the FOV from the projection matrix +static inline vec4 extractFov( const glm::mat4& m) { + static const std::array CLIPS{ { + { 1, 0, 0, 1 }, + { -1, 0, 0, 1 }, + { 0, 1, 0, 1 }, + { 0, -1, 0, 1 } + } }; + + glm::mat4 mt = glm::transpose(m); + vec4 v, result; + // Left + v = mt * CLIPS[0]; + result.x = -atanf(v.z / v.x); + // Right + v = mt * CLIPS[1]; + result.y = atanf(v.z / v.x); + // Down + v = mt * CLIPS[2]; + result.z = -atanf(v.z / v.y); + // Up + v = mt * CLIPS[3]; + result.w = atanf(v.z / v.y); + return result; +} + + class GLESFramebuffer : public gl::GLFramebuffer { using Parent = gl::GLFramebuffer; static GLuint allocate() { @@ -29,6 +57,24 @@ public: GLint currentFBO = -1; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤tFBO); glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + + vec2 focalPoint{ -1.0f }; + +#if 0 + { + auto backend = _backend.lock(); + if (backend && backend->isStereo()) { + glm::mat4 projections[2]; + backend->getStereoProjections(projections); + vec4 fov = extractFov(projections[0]); + float fovwidth = fov.x + fov.y; + float fovheight = fov.z + fov.w; + focalPoint.x = fov.y / fovwidth; + focalPoint.y = (fov.z / fovheight) - 0.5f; + } + } +#endif + gl::GLTexture* gltexture = nullptr; TexturePointer surface; if (_gpuObject.getColorStamps() != _colorStamps) { @@ -58,7 +104,7 @@ public: surface = b._texture; if (surface) { Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -66,6 +112,24 @@ public: if (gltexture) { if (gltexture->_target == GL_TEXTURE_2D) { glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); +#if 0 + if (glTextureFoveationParametersQCOM && focalPoint.x != -1.0f) { + static GLint FOVEATION_QUERY = 0; + static std::once_flag once; + std::call_once(once, [&]{ + glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_FOVEATED_FEATURE_QUERY_QCOM, &FOVEATION_QUERY); + }); + static const float foveaArea = 4.0f; + static const float gain = 16.0f; + GLESBackend::GLESTexture* glestexture = static_cast(gltexture); + glestexture->withPreservedTexture([=]{ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_FOVEATED_FEATURE_BITS_QCOM, GL_FOVEATION_ENABLE_BIT_QCOM | GL_FOVEATION_SCALED_BIN_METHOD_BIT_QCOM); + glTextureFoveationParametersQCOM(_id, 0, 0, -focalPoint.x, focalPoint.y, gain * 2.0f, gain, foveaArea); + glTextureFoveationParametersQCOM(_id, 0, 1, focalPoint.x, focalPoint.y, gain * 2.0f, gain, foveaArea); + }); + + } +#endif } else { glFramebufferTextureLayer(GL_FRAMEBUFFER, colorAttachments[unit], gltexture->_texture, 0, b._subresource); diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index b080b0ceac..7109b3dfeb 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -66,6 +66,7 @@ public: virtual void syncProgram(const gpu::ShaderPointer& program) = 0; virtual void recycle() const = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; + virtual void setCameraCorrection(const Mat4& correction, const Mat4& prevRenderView, bool reset = false) {} virtual bool supportedTextureFormat(const gpu::Element& format) = 0; @@ -117,7 +118,6 @@ public: static ContextMetricSize textureResourcePopulatedGPUMemSize; static ContextMetricSize textureResourceIdealGPUMemSize; -protected: virtual bool isStereo() const { return _stereo.isStereo(); } @@ -127,6 +127,7 @@ protected: eyeProjections[i] = _stereo._eyeProjections[i]; } } +protected: void getStereoViews(mat4* eyeViews) const { for (int i = 0; i < 2; ++i) { From 67cf08e8aed58f9268b00289e26761b137b508e5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 10:24:30 -0800 Subject: [PATCH 051/130] Quest frame player --- android/apps/questFramePlayer/CMakeLists.txt | 9 + android/apps/questFramePlayer/build.gradle | 51 ++ .../apps/questFramePlayer/proguard-rules.pro | 25 + .../src/main/AndroidManifest.xml | 55 ++ .../src/main/cpp/PlayerWindow.cpp | 25 + .../src/main/cpp/PlayerWindow.h | 29 + .../src/main/cpp/RenderThread.cpp | 240 ++++++ .../src/main/cpp/RenderThread.h | 44 ++ .../questFramePlayer/src/main/cpp/main.cpp | 56 ++ .../frameplayer/QuestQtActivity.java | 53 ++ .../frameplayer/QuestRenderActivity.java | 14 + .../src/main/res/drawable/ic_launcher.xml | 17 + .../src/main/res/values/strings.xml | 3 + android/libraries/oculus/build.gradle | 17 + .../oculus/src/main/AndroidManifest.xml | 2 + .../oculus/OculusMobileActivity.java | 103 +++ android/settings.gradle | 22 +- hifi_android.py | 7 + libraries/oculusMobile/CMakeLists.txt | 11 + libraries/oculusMobile/src/ovr/Forward.h | 17 + .../oculusMobile/src/ovr/Framebuffer.cpp | 93 +++ libraries/oculusMobile/src/ovr/Framebuffer.h | 34 + libraries/oculusMobile/src/ovr/GLContext.cpp | 182 +++++ libraries/oculusMobile/src/ovr/GLContext.h | 37 + libraries/oculusMobile/src/ovr/Helpers.cpp | 38 + libraries/oculusMobile/src/ovr/Helpers.h | 94 +++ libraries/oculusMobile/src/ovr/TaskQueue.cpp | 40 + libraries/oculusMobile/src/ovr/TaskQueue.h | 42 ++ libraries/oculusMobile/src/ovr/VrHandler.cpp | 337 +++++++++ libraries/oculusMobile/src/ovr/VrHandler.h | 47 ++ libraries/oculusMobilePlugin/CMakeLists.txt | 29 + libraries/oculusMobilePlugin/src/Logging.cpp | 4 + libraries/oculusMobilePlugin/src/Logging.h | 13 + .../src/OculusMobileControllerManager.cpp | 694 ++++++++++++++++++ .../src/OculusMobileControllerManager.h | 43 ++ .../src/OculusMobileDisplayPlugin.cpp | 269 +++++++ .../src/OculusMobileDisplayPlugin.h | 65 ++ 37 files changed, 2859 insertions(+), 2 deletions(-) create mode 100644 android/apps/questFramePlayer/CMakeLists.txt create mode 100644 android/apps/questFramePlayer/build.gradle create mode 100644 android/apps/questFramePlayer/proguard-rules.pro create mode 100644 android/apps/questFramePlayer/src/main/AndroidManifest.xml create mode 100644 android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp create mode 100644 android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h create mode 100644 android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp create mode 100644 android/apps/questFramePlayer/src/main/cpp/RenderThread.h create mode 100644 android/apps/questFramePlayer/src/main/cpp/main.cpp create mode 100644 android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java create mode 100644 android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java create mode 100644 android/apps/questFramePlayer/src/main/res/drawable/ic_launcher.xml create mode 100644 android/apps/questFramePlayer/src/main/res/values/strings.xml create mode 100644 android/libraries/oculus/build.gradle create mode 100644 android/libraries/oculus/src/main/AndroidManifest.xml create mode 100644 android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java create mode 100644 libraries/oculusMobile/CMakeLists.txt create mode 100644 libraries/oculusMobile/src/ovr/Forward.h create mode 100644 libraries/oculusMobile/src/ovr/Framebuffer.cpp create mode 100644 libraries/oculusMobile/src/ovr/Framebuffer.h create mode 100644 libraries/oculusMobile/src/ovr/GLContext.cpp create mode 100644 libraries/oculusMobile/src/ovr/GLContext.h create mode 100644 libraries/oculusMobile/src/ovr/Helpers.cpp create mode 100644 libraries/oculusMobile/src/ovr/Helpers.h create mode 100644 libraries/oculusMobile/src/ovr/TaskQueue.cpp create mode 100644 libraries/oculusMobile/src/ovr/TaskQueue.h create mode 100644 libraries/oculusMobile/src/ovr/VrHandler.cpp create mode 100644 libraries/oculusMobile/src/ovr/VrHandler.h create mode 100644 libraries/oculusMobilePlugin/CMakeLists.txt create mode 100644 libraries/oculusMobilePlugin/src/Logging.cpp create mode 100644 libraries/oculusMobilePlugin/src/Logging.h create mode 100644 libraries/oculusMobilePlugin/src/OculusMobileControllerManager.cpp create mode 100644 libraries/oculusMobilePlugin/src/OculusMobileControllerManager.h create mode 100644 libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp create mode 100644 libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h diff --git a/android/apps/questFramePlayer/CMakeLists.txt b/android/apps/questFramePlayer/CMakeLists.txt new file mode 100644 index 0000000000..5889585a6c --- /dev/null +++ b/android/apps/questFramePlayer/CMakeLists.txt @@ -0,0 +1,9 @@ +set(TARGET_NAME questFramePlayer) +setup_hifi_library(AndroidExtras) +link_hifi_libraries(shared ktx shaders gpu gl oculusMobile ${PLATFORM_GL_BACKEND}) +target_include_directories(${TARGET_NAME} PRIVATE ${HIFI_ANDROID_PRECOMPILED}/ovr/VrApi/Include) +target_link_libraries(${TARGET_NAME} android log m) +target_opengl() +target_oculus_mobile() + + diff --git a/android/apps/questFramePlayer/build.gradle b/android/apps/questFramePlayer/build.gradle new file mode 100644 index 0000000000..13d806c3a4 --- /dev/null +++ b/android/apps/questFramePlayer/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'com.android.application' + +android { + signingConfigs { + release { + keyAlias 'key0' + keyPassword 'password' + storeFile file('C:/android/keystore.jks') + storePassword 'password' + } + } + + compileSdkVersion 28 + defaultConfig { + applicationId "io.highfidelity.frameplayer" + minSdkVersion 25 + targetSdkVersion 28 + ndk { abiFilters 'arm64-v8a' } + externalNativeBuild { + cmake { + arguments '-DHIFI_ANDROID=1', + '-DHIFI_ANDROID_APP=questFramePlayer', + '-DANDROID_TOOLCHAIN=clang', + '-DANDROID_STL=c++_shared', + + '-DCMAKE_VERBOSE_MAKEFILE=ON' + targets = ['questFramePlayer'] + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + + externalNativeBuild.cmake.path '../../../CMakeLists.txt' +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs') + implementation project(':oculus') + implementation project(':qt') +} diff --git a/android/apps/questFramePlayer/proguard-rules.pro b/android/apps/questFramePlayer/proguard-rules.pro new file mode 100644 index 0000000000..b3c0078513 --- /dev/null +++ b/android/apps/questFramePlayer/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/apps/questFramePlayer/src/main/AndroidManifest.xml b/android/apps/questFramePlayer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..721e8cee89 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp new file mode 100644 index 0000000000..ec2986298e --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp @@ -0,0 +1,25 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// 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 "PlayerWindow.h" + +#include + +PlayerWindow::PlayerWindow() { + installEventFilter(this); + setFlags(Qt::MSWindowsOwnDC | Qt::Window | Qt::Dialog | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint); + setSurfaceType(QSurface::OpenGLSurface); + create(); + showFullScreen(); + // Ensure the window is visible and the GL context is valid + QCoreApplication::processEvents(); + _renderThread.initialize(this); +} + +PlayerWindow::~PlayerWindow() { +} diff --git a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h new file mode 100644 index 0000000000..e4dd6cef43 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h @@ -0,0 +1,29 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// 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 +// + +#pragma once +#include +#include + +#include +#include "RenderThread.h" + +// Create a simple OpenGL window that renders text in various ways +class PlayerWindow : public QWindow { +public: + PlayerWindow(); + virtual ~PlayerWindow(); + +protected: + //bool eventFilter(QObject* obj, QEvent* event) override; + //void keyPressEvent(QKeyEvent* event) override; + +private: + QSettings _settings; + RenderThread _renderThread; +}; diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp new file mode 100644 index 0000000000..5eabe6b9b1 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp @@ -0,0 +1,240 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// 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 "RenderThread.h" + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +static JNIEnv* _env { nullptr }; +static JavaVM* _vm { nullptr }; +static jobject _activity { nullptr }; + +struct HandController{ + ovrInputTrackedRemoteCapabilities caps {}; + ovrInputStateTrackedRemote state {}; + ovrResult stateResult{ ovrSuccess }; + ovrTracking tracking {}; + ovrResult trackingResult{ ovrSuccess }; + + void update(ovrMobile* session, double time = 0.0) { + const auto& deviceId = caps.Header.DeviceID; + stateResult = vrapi_GetCurrentInputState(session, deviceId, &state.Header); + trackingResult = vrapi_GetInputTrackingState(session, deviceId, 0.0, &tracking); + } +}; + +std::vector devices; + +extern "C" { + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { + __android_log_write(ANDROID_LOG_WARN, "QQQ", __FUNCTION__); + return JNI_VERSION_1_6; +} + + +JNIEXPORT void JNICALL Java_io_highfidelity_frameplayer_QuestQtActivity_nativeOnCreate(JNIEnv* env, jobject obj) { + env->GetJavaVM(&_vm); + _activity = env->NewGlobalRef(obj); +} +} + +static const char* FRAME_FILE = "assets:/frames/20190121_1220.json"; + +static void textureLoader(const std::string& filename, const gpu::TexturePointer& texture, uint16_t layer) { + QImage image; + QImageReader(filename.c_str()).read(&image); + if (layer > 0) { + return; + } + texture->assignStoredMip(0, image.byteCount(), image.constBits()); +} + +void RenderThread::submitFrame(const gpu::FramePointer& frame) { + std::unique_lock lock(_frameLock); + _pendingFrames.push(frame); +} + +void RenderThread::move(const glm::vec3& v) { + std::unique_lock lock(_frameLock); + _correction = glm::inverse(glm::translate(mat4(), v)) * _correction; +} + +void RenderThread::initialize(QWindow* window) { + std::unique_lock lock(_frameLock); + setObjectName("RenderThread"); + Parent::initialize(); + _window = window; + _thread->setObjectName("RenderThread"); +} + +void RenderThread::setup() { + // Wait until the context has been moved to this thread + { std::unique_lock lock(_frameLock); } + + + ovr::VrHandler::initVr(); + __android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity"); + _vm->AttachCurrentThread(&_env, nullptr); + jclass cls = _env->GetObjectClass(_activity); + jmethodID mid = _env->GetMethodID(cls, "launchOculusActivity", "()V"); + _env->CallVoidMethod(_activity, mid); + __android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity done"); + ovr::VrHandler::setHandler(this); + + makeCurrent(); + + // GPU library init + gpu::Context::init(); + _gpuContext = std::make_shared(); + _backend = _gpuContext->getBackend(); + _gpuContext->beginFrame(); + _gpuContext->endFrame(); + + makeCurrent(); + glGenTextures(1, &_externalTexture); + glBindTexture(GL_TEXTURE_2D, _externalTexture); + static const glm::u8vec4 color{ 0,1,0,0 }; + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &color); + + if (QFileInfo(FRAME_FILE).exists()) { + auto frame = gpu::readFrame(FRAME_FILE, _externalTexture, &textureLoader); + submitFrame(frame); + } +} + +void RenderThread::shutdown() { + _activeFrame.reset(); + while (!_pendingFrames.empty()) { + _gpuContext->consumeFrameUpdates(_pendingFrames.front()); + _pendingFrames.pop(); + } + _gpuContext->shutdown(); + _gpuContext.reset(); +} + +void RenderThread::handleInput() { + static std::once_flag once; + std::call_once(once, [&]{ + withOvrMobile([&](ovrMobile* session){ + int deviceIndex = 0; + ovrInputCapabilityHeader capsHeader; + while (vrapi_EnumerateInputDevices(session, deviceIndex, &capsHeader) >= 0) { + if (capsHeader.Type == ovrControllerType_TrackedRemote) { + HandController controller = {}; + controller.caps.Header = capsHeader; + controller.state.Header.ControllerType = ovrControllerType_TrackedRemote; + vrapi_GetInputDeviceCapabilities( session, &controller.caps.Header); + devices.push_back(controller); + } + ++deviceIndex; + } + }); + }); + + auto readResult = ovr::VrHandler::withOvrMobile([&](ovrMobile *session) { + for (auto &controller : devices) { + controller.update(session); + } + }); + + if (readResult) { + for (auto &controller : devices) { + const auto &caps = controller.caps; + if (controller.stateResult >= 0) { + const auto &remote = controller.state; + if (remote.Joystick.x != 0.0f || remote.Joystick.y != 0.0f) { + glm::vec3 translation; + float rotation = 0.0f; + if (caps.ControllerCapabilities & ovrControllerCaps_LeftHand) { + translation = glm::vec3{0.0f, -remote.Joystick.y, 0.0f}; + } else { + translation = glm::vec3{remote.Joystick.x, 0.0f, -remote.Joystick.y}; + } + float scale = 0.1f + (1.9f * remote.GripTrigger); + _correction = glm::translate(glm::mat4(), translation * scale) * _correction; + } + } + } + } +} + +void RenderThread::renderFrame() { + GLuint finalTexture = 0; + glm::uvec2 finalTextureSize; + const auto& tracking = beginFrame(); + if (_activeFrame) { + const auto& frame = _activeFrame; + auto& eyeProjections = frame->stereoState._eyeProjections; + auto& eyeOffsets = frame->stereoState._eyeViews; + // Quest + auto frameCorrection = _correction * ovr::toGlm(tracking.HeadPose.Pose); + _backend->setCameraCorrection(glm::inverse(frameCorrection), frame->view); + ovr::for_each_eye([&](ovrEye eye){ + const auto& eyeInfo = tracking.Eye[eye]; + eyeProjections[eye] = ovr::toGlm(eyeInfo.ProjectionMatrix); + eyeOffsets[eye] = ovr::toGlm(eyeInfo.ViewMatrix); + }); + _backend->recycle(); + _backend->syncCache(); + _gpuContext->enableStereo(true); + if (frame && !frame->batches.empty()) { + _gpuContext->executeFrame(frame); + } + auto& glbackend = (gpu::gl::GLBackend&)(*_backend); + finalTextureSize = { frame->framebuffer->getWidth(), frame->framebuffer->getHeight() }; + finalTexture = glbackend.getTextureID(frame->framebuffer->getRenderBuffer(0)); + } + presentFrame(finalTexture, finalTextureSize, tracking); +} + +bool RenderThread::process() { + pollTask(); + + if (!vrActive()) { + QThread::msleep(1); + return true; + } + + std::queue pendingFrames; + { + std::unique_lock lock(_frameLock); + pendingFrames.swap(_pendingFrames); + } + + makeCurrent(); + while (!pendingFrames.empty()) { + _activeFrame = pendingFrames.front(); + pendingFrames.pop(); + _gpuContext->consumeFrameUpdates(_activeFrame); + _activeFrame->stereoState._enable = true; + } + + handleInput(); + renderFrame(); + return true; +} diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.h b/android/apps/questFramePlayer/src/main/cpp/RenderThread.h new file mode 100644 index 0000000000..701cd25f5b --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.h @@ -0,0 +1,44 @@ +// +// Created by Bradley Austin Davis on 2018/10/21 +// 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 +// + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +class RenderThread : public GenericThread, ovr::VrHandler { + using Parent = GenericThread; +public: + QWindow* _window{ nullptr }; + std::mutex _mutex; + gpu::ContextPointer _gpuContext; // initialized during window creation + std::shared_ptr _backend; + std::atomic _presentCount{ 0 }; + std::mutex _frameLock; + std::queue _pendingFrames; + gpu::FramePointer _activeFrame; + uint32_t _externalTexture{ 0 }; + glm::mat4 _correction; + + void move(const glm::vec3& v); + void setup() override; + bool process() override; + void shutdown() override; + + void handleInput(); + + void submitFrame(const gpu::FramePointer& frame); + void initialize(QWindow* window); + void renderFrame(); +}; diff --git a/android/apps/questFramePlayer/src/main/cpp/main.cpp b/android/apps/questFramePlayer/src/main/cpp/main.cpp new file mode 100644 index 0000000000..4730d3fa15 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/main.cpp @@ -0,0 +1,56 @@ +// +// Created by Bradley Austin Davis on 2018/11/22 +// 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 "PlayerWindow.h" + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + if (!message.isEmpty()) { + const char * local=message.toStdString().c_str(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } + } +} + +int main(int argc, char** argv) { + setupHifiApplication("gpuFramePlayer"); + QGuiApplication app(argc, argv); + auto oldMessageHandler = qInstallMessageHandler(messageHandler); + DependencyManager::set(); + PlayerWindow window; + __android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec"); + app.exec(); + __android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec done"); + qInstallMessageHandler(oldMessageHandler); + return 0; +} + + diff --git a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java new file mode 100644 index 0000000000..d498e27547 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java @@ -0,0 +1,53 @@ +// +// Created by Bradley Austin Davis on 2018/11/20 +// Copyright 2013-2018 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 +// +package io.highfidelity.frameplayer; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import org.qtproject.qt5.android.bindings.QtActivity; + +import io.highfidelity.oculus.OculusMobileActivity; + + +public class QuestQtActivity extends QtActivity { + private native void nativeOnCreate(); + private boolean launchedQuestMode = false; + + @Override + public void onCreate(Bundle savedInstanceState) { + Log.w("QQQ_Qt", "QuestQtActivity::onCreate"); + super.onCreate(savedInstanceState); + nativeOnCreate(); + } + + @Override + public void onDestroy() { + Log.w("QQQ_Qt", "QuestQtActivity::onDestroy"); + super.onDestroy(); + } + + public void launchOculusActivity() { + Log.w("QQQ_Qt", "QuestQtActivity::launchOculusActivity"); + runOnUiThread(()->{ + keepInterfaceRunning = true; + launchedQuestMode = true; + moveTaskToBack(true); + startActivity(new Intent(this, QuestRenderActivity.class)); + }); + } + + @Override + public void onResume() { + super.onResume(); + if (launchedQuestMode) { + moveTaskToBack(true); + } + } +} diff --git a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java new file mode 100644 index 0000000000..a395a32b68 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java @@ -0,0 +1,14 @@ +package io.highfidelity.frameplayer; + +import android.content.Intent; +import android.os.Bundle; + +import io.highfidelity.oculus.OculusMobileActivity; + +public class QuestRenderActivity extends OculusMobileActivity { + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + startActivity(new Intent(this, QuestQtActivity.class)); + } +} diff --git a/android/apps/questFramePlayer/src/main/res/drawable/ic_launcher.xml b/android/apps/questFramePlayer/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000000..03b1edc4e9 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/android/apps/questFramePlayer/src/main/res/values/strings.xml b/android/apps/questFramePlayer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..8bf550f74e --- /dev/null +++ b/android/apps/questFramePlayer/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + GPU Frame Player + diff --git a/android/libraries/oculus/build.gradle b/android/libraries/oculus/build.gradle new file mode 100644 index 0000000000..b072f99eb7 --- /dev/null +++ b/android/libraries/oculus/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} diff --git a/android/libraries/oculus/src/main/AndroidManifest.xml b/android/libraries/oculus/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..57df1a4226 --- /dev/null +++ b/android/libraries/oculus/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java new file mode 100644 index 0000000000..01d74ea94d --- /dev/null +++ b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java @@ -0,0 +1,103 @@ +// +// Created by Bradley Austin Davis on 2018/11/20 +// Copyright 2013-2018 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 +// +package io.highfidelity.oculus; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.WindowManager; + +/** + * Contains a native surface and forwards the activity lifecycle and surface lifecycle + * events to the OculusMobileDisplayPlugin + */ +public class OculusMobileActivity extends Activity implements SurfaceHolder.Callback { + private static final String TAG = OculusMobileActivity.class.getSimpleName(); + static { System.loadLibrary("oculusMobile"); } + private native void nativeOnCreate(); + private native static void nativeOnResume(); + private native static void nativeOnPause(); + private native static void nativeOnDestroy(); + private native static void nativeOnSurfaceChanged(Surface s); + + private SurfaceView mView; + private SurfaceHolder mSurfaceHolder; + + + public static void launch(Activity activity) { + if (activity != null) { + activity.runOnUiThread(()->{ + activity.startActivity(new Intent(activity, OculusMobileActivity.class)); + }); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + Log.w(TAG, "QQQ onCreate"); + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + // Create a native surface for VR rendering (Qt GL surfaces are not suitable + // because of the lack of fine control over the surface callbacks) + mView = new SurfaceView(this); + setContentView(mView); + mView.getHolder().addCallback(this); + + // Forward the create message to the JNI code + nativeOnCreate(); + } + + @Override + protected void onDestroy() { + Log.w(TAG, "QQQ onDestroy"); + if (mSurfaceHolder != null) { + nativeOnSurfaceChanged(null); + } + nativeOnDestroy(); + super.onDestroy(); + } + + @Override + protected void onResume() { + Log.w(TAG, "QQQ onResume"); + super.onResume(); + nativeOnResume(); + } + + @Override + protected void onPause() { + Log.w(TAG, "QQQ onPause"); + nativeOnPause(); + super.onPause(); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.w(TAG, "QQQ surfaceCreated"); + nativeOnSurfaceChanged(holder.getSurface()); + mSurfaceHolder = holder; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.w(TAG, "QQQ surfaceChanged"); + nativeOnSurfaceChanged(holder.getSurface()); + mSurfaceHolder = holder; + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.w(TAG, "QQQ surfaceDestroyed"); + nativeOnSurfaceChanged(null); + mSurfaceHolder = null; + } +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 1e7b3c768a..699f617cce 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,8 +1,26 @@ +// +// Libraries +// + +include ':oculus' +project(':oculus').projectDir = new File(settingsDir, 'libraries/oculus') + include ':qt' project(':qt').projectDir = new File(settingsDir, 'libraries/qt') +// +// Applications +// + include ':interface' project(':interface').projectDir = new File(settingsDir, 'apps/interface') -//include ':framePlayer' -//project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer') +// +// Test projects +// + +include ':framePlayer' +project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer') + +include ':questFramePlayer' +project(':questFramePlayer').projectDir = new File(settingsDir, 'apps/questFramePlayer') diff --git a/hifi_android.py b/hifi_android.py index 13c9cdccf2..2e6a42d127 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -52,6 +52,13 @@ ANDROID_PACKAGES = { 'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release', 'includeLibs': ['libvrapi.so'] }, + 'oculusPlatform': { + 'file': 'OVRPlatformSDK_v1.32.0.zip', + 'versionId': 'jG9DB16zOGxSrmtZy4jcQnwO0TJUuaeL', + 'checksum': 'ab5b203b3a39a56ab148d68fff769e05', + 'sharedLibFolder': 'Android/libs/arm64-v8a', + 'includeLibs': ['libovrplatformloader.so'] + }, 'openssl': { 'file': 'openssl-1.1.0g_armv8.tgz', 'versionId': 'AiiPjmgUZTgNj7YV1EEx2lL47aDvvvAW', diff --git a/libraries/oculusMobile/CMakeLists.txt b/libraries/oculusMobile/CMakeLists.txt new file mode 100644 index 0000000000..213721722c --- /dev/null +++ b/libraries/oculusMobile/CMakeLists.txt @@ -0,0 +1,11 @@ +if (ANDROID) + set(TARGET_NAME oculusMobile) + # don't use the setup_hifi_library macro, we don't want ANY qt dependencies + file(GLOB_RECURSE LIB_SRCS "src/*.h" "src/*.cpp" "src/*.c" "src/*.qrc") + add_library(${TARGET_NAME} SHARED ${LIB_SRCS}) + target_glm() + target_egl() + target_glad() + target_oculus_mobile() + target_link_libraries(${TARGET_NAME} android log) +endif() diff --git a/libraries/oculusMobile/src/ovr/Forward.h b/libraries/oculusMobile/src/ovr/Forward.h new file mode 100644 index 0000000000..5881dde9a7 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/Forward.h @@ -0,0 +1,17 @@ +// +// Created by Bradley Austin Davis on 2018/11/23 +// Copyright 2013-2018 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 +// +#pragma once +#include +#include + +namespace ovr { + using Mutex = std::mutex; + using Condition = std::condition_variable; + using Lock = std::unique_lock; + using Task = std::function; +} \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp new file mode 100644 index 0000000000..4c4fd2a983 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -0,0 +1,93 @@ +// +// Created by Bradley Austin Davis on 2018/11/20 +// Copyright 2013-2018 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 "Framebuffer.h" + +#include +#include +#include + +#include +#include + +using namespace ovr; + +void Framebuffer::updateLayer(int eye, ovrLayerProjection2& layer, const ovrMatrix4f* projectionMatrix ) const { + auto& layerTexture = layer.Textures[eye]; + layerTexture.ColorSwapChain = _swapChain; + layerTexture.SwapChainIndex = _index; + if (projectionMatrix) { + layerTexture.TexCoordsFromTanAngles = ovrMatrix4f_TanAngleMatrixFromProjection( projectionMatrix ); + } + layerTexture.TextureRect = { 0, 0, 1, 1 }; +} + +void Framebuffer::create(const glm::uvec2& size) { + _size = size; + _index = 0; + _validTexture = false; + + // Depth renderbuffer + glGenRenderbuffers(1, &_depth); + glBindRenderbuffer(GL_RENDERBUFFER, _depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, _size.x, _size.y); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + // Framebuffer + glGenFramebuffers(1, &_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + _swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, _size.x, _size.y, 1, 3); + _length = vrapi_GetTextureSwapChainLength(_swapChain); + if (!_length) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); + return; + } + + for (int i = 0; i < _length; ++i) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, i); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Framebuffer::destroy() { + if (0 != _fbo) { + glDeleteFramebuffers(1, &_fbo); + _fbo = 0; + } + if (0 != _depth) { + glDeleteRenderbuffers(1, &_depth); + _depth = 0; + } + if (_swapChain != nullptr) { + vrapi_DestroyTextureSwapChain(_swapChain); + _swapChain = nullptr; + } + _index = -1; + _length = -1; +} + +void Framebuffer::advance() { + _index = (_index + 1) % _length; + _validTexture = false; +} + +void Framebuffer::bind() { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + if (!_validTexture) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, _index); + glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, chainTexId, 0); + _validTexture = true; + } +} diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.h b/libraries/oculusMobile/src/ovr/Framebuffer.h new file mode 100644 index 0000000000..5127574462 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/Framebuffer.h @@ -0,0 +1,34 @@ +// +// Created by Bradley Austin Davis on 2018/11/20 +// Copyright 2013-2018 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 +// +#pragma once + +#include +#include + +#include + +namespace ovr { + +struct Framebuffer { +public: + void updateLayer(int eye, ovrLayerProjection2& layer, const ovrMatrix4f* projectionMatrix = nullptr) const; + void create(const glm::uvec2& size); + void advance(); + void destroy(); + void bind(); + + uint32_t _depth { 0 }; + uint32_t _fbo{ 0 }; + int _length{ -1 }; + int _index{ -1 }; + bool _validTexture{ false }; + glm::uvec2 _size; + ovrTextureSwapChain* _swapChain{ nullptr }; +}; + +} // namespace ovr \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/GLContext.cpp b/libraries/oculusMobile/src/ovr/GLContext.cpp new file mode 100644 index 0000000000..449ba67084 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/GLContext.cpp @@ -0,0 +1,182 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 "GLContext.h" + +#include +#include +#include + +#include + +#if !defined(EGL_OPENGL_ES3_BIT_KHR) +#define EGL_OPENGL_ES3_BIT_KHR 0x0040 +#endif + +using namespace ovr; + +static void* getGlProcessAddress(const char *namez) { + auto result = eglGetProcAddress(namez); + return (void*)result; +} + + +void GLContext::initModule() { + static std::once_flag once; + std::call_once(once, [&]{ + gladLoadGLES2Loader(getGlProcessAddress); + }); +} + +void APIENTRY debugMessageCallback(GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar* message, + const void* userParam) { + if (type == GL_DEBUG_TYPE_PERFORMANCE_KHR) { + return; + } + switch (severity) { + case GL_DEBUG_SEVERITY_HIGH: + case GL_DEBUG_SEVERITY_MEDIUM: + break; + default: + return; + } + + __android_log_write(ANDROID_LOG_WARN, "QQQ_GL", message); +} + +GLContext::~GLContext() { + destroy(); +} + +EGLConfig GLContext::findConfig(EGLDisplay display) { + // Do NOT use eglChooseConfig, because the Android EGL code pushes in multisample + // flags in eglChooseConfig if the user has selected the "force 4x MSAA" option in + // settings, and that is completely wasted for our warp target. + std::vector configs; + { + const int MAX_CONFIGS = 1024; + EGLConfig configsBuffer[MAX_CONFIGS]; + EGLint numConfigs = 0; + if (eglGetConfigs(display, configsBuffer, MAX_CONFIGS, &numConfigs) == EGL_FALSE) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_GL", "Failed to fetch configs"); + return 0; + } + configs.resize(numConfigs); + memcpy(configs.data(), configsBuffer, sizeof(EGLConfig) * numConfigs); + } + + std::vector> configAttribs{ + { EGL_RED_SIZE, 8 }, { EGL_GREEN_SIZE, 8 }, { EGL_BLUE_SIZE, 8 }, { EGL_ALPHA_SIZE, 8 }, + { EGL_DEPTH_SIZE, 0 }, { EGL_STENCIL_SIZE, 0 }, { EGL_SAMPLES, 0 }, + }; + + auto matchAttrib = [&](EGLConfig config, const std::pair& attribAndValue) { + EGLint value = 0; + eglGetConfigAttrib(display, config, attribAndValue.first, &value); + return (attribAndValue.second == value); + }; + + auto matchAttribFlags = [&](EGLConfig config, const std::pair& attribAndValue) { + EGLint value = 0; + eglGetConfigAttrib(display, config, attribAndValue.first, &value); + return (value & attribAndValue.second) == attribAndValue.second; + }; + + auto matchConfig = [&](EGLConfig config) { + if (!matchAttribFlags(config, { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR})) { + return false; + } + // The pbuffer config also needs to be compatible with normal window rendering + // so it can share textures with the window context. + if (!matchAttribFlags(config, { EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT})) { + return false; + } + + for (const auto& attrib : configAttribs) { + if (!matchAttrib(config, attrib)) { + return false; + } + } + + return true; + }; + + + for (const auto& config : configs) { + if (matchConfig(config)) { + return config; + } + } + + return 0; +} + +bool GLContext::makeCurrent() { + return eglMakeCurrent(display, surface, surface, context) != EGL_FALSE; +} + +void GLContext::doneCurrent() { + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +bool GLContext::create(EGLDisplay display, EGLContext shareContext) { + this->display = display; + + auto config = findConfig(display); + + if (config == 0) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_GL", "Failed eglChooseConfig"); + return false; + } + + EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + + context = eglCreateContext(display, config, shareContext, contextAttribs); + if (context == EGL_NO_CONTEXT) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_GL", "Failed eglCreateContext"); + return false; + } + + const EGLint surfaceAttribs[] = { EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE }; + surface = eglCreatePbufferSurface(display, config, surfaceAttribs); + if (surface == EGL_NO_SURFACE) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_GL", "Failed eglCreatePbufferSurface"); + return false; + } + + if (!makeCurrent()) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_GL", "Failed eglMakeCurrent"); + return false; + } + + ovr::GLContext::initModule(); + +#ifndef NDEBUG + glDebugMessageCallback(debugMessageCallback, this); + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); +#endif + return true; +} + +void GLContext::destroy() { + if (context != EGL_NO_CONTEXT) { + eglDestroyContext(display, context); + context = EGL_NO_CONTEXT; + } + + if (surface != EGL_NO_SURFACE) { + eglDestroySurface(display, surface); + surface = EGL_NO_SURFACE; + } +} diff --git a/libraries/oculusMobile/src/ovr/GLContext.h b/libraries/oculusMobile/src/ovr/GLContext.h new file mode 100644 index 0000000000..04f96e8d47 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/GLContext.h @@ -0,0 +1,37 @@ +// +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 +// +#pragma once + +#include + +#include +#include + +namespace ovr { + +struct GLContext { + using Pointer = std::shared_ptr; + EGLSurface surface{ EGL_NO_SURFACE }; + EGLContext context{ EGL_NO_CONTEXT }; + EGLDisplay display{ EGL_NO_DISPLAY }; + + ~GLContext(); + static EGLConfig findConfig(EGLDisplay display); + bool makeCurrent(); + void doneCurrent(); + bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT); + void destroy(); + operator bool() const { return context != EGL_NO_CONTEXT; } + static void initModule(); +}; + +} + + +#define CHECK_GL_ERROR() if(false) {} \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/Helpers.cpp b/libraries/oculusMobile/src/ovr/Helpers.cpp new file mode 100644 index 0000000000..a48d37311e --- /dev/null +++ b/libraries/oculusMobile/src/ovr/Helpers.cpp @@ -0,0 +1,38 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 "Helpers.h" + +#include +#include +#include +#include + +using namespace ovr; + +void Fov::extend(const Fov& other) { + for (size_t i = 0; i < 4; ++i) { + leftRightUpDown[i] = std::max(leftRightUpDown[i], other.leftRightUpDown[i]); + } +} + +void Fov::extract(const ovrMatrix4f& mat) { + auto& fs = leftRightUpDown; + ovrMatrix4f_ExtractFov( &mat, fs, fs + 1, fs + 2, fs + 3); +} + +glm::mat4 Fov::withZ(float nearZ, float farZ) const { + const auto& fs = leftRightUpDown; + return ovr::toGlm(ovrMatrix4f_CreateProjectionAsymmetricFov(fs[0], fs[1], fs[2], fs[3], nearZ, farZ)); +} + +glm::mat4 Fov::withZ(const glm::mat4& other) const { + // FIXME + return withZ(0.01f, 1000.0f); +} + diff --git a/libraries/oculusMobile/src/ovr/Helpers.h b/libraries/oculusMobile/src/ovr/Helpers.h new file mode 100644 index 0000000000..2bd0b7f603 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/Helpers.h @@ -0,0 +1,94 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 +// +#pragma once + +#include +#include +#include +#include +#include + +namespace ovr { + +struct Fov { + float leftRightUpDown[4]; + Fov() {} + Fov(const ovrMatrix4f& mat) { extract(mat); } + void extract(const ovrMatrix4f& mat); + void extend(const Fov& other); + glm::mat4 withZ(const glm::mat4& other) const; + glm::mat4 withZ(float nearZ, float farZ) const; +}; + +// Convenience method for looping over each eye with a lambda +static inline void for_each_eye(const std::function& f) { + f(VRAPI_EYE_LEFT); + f(VRAPI_EYE_RIGHT); +} + +static inline void for_each_hand(const std::function& f) { + f(VRAPI_HAND_LEFT); + f(VRAPI_HAND_RIGHT); +} + +static inline glm::mat4 toGlm(const ovrMatrix4f& om) { + return glm::transpose(glm::make_mat4(&om.M[0][0])); +} + +static inline glm::vec3 toGlm(const ovrVector3f& ov) { + return glm::make_vec3(&ov.x); +} + +static inline glm::vec2 toGlm(const ovrVector2f& ov) { + return glm::make_vec2(&ov.x); +} + +static inline glm::quat toGlm(const ovrQuatf& oq) { + return glm::make_quat(&oq.x); +} + +static inline glm::mat4 toGlm(const ovrPosef& op) { + glm::mat4 orientation = glm::mat4_cast(toGlm(op.Orientation)); + glm::mat4 translation = glm::translate(glm::mat4(), toGlm(op.Position)); + return translation * orientation; +} + +static inline ovrMatrix4f fromGlm(const glm::mat4& m) { + ovrMatrix4f result; + glm::mat4 transposed(glm::transpose(m)); + memcpy(result.M, &(transposed[0][0]), sizeof(float) * 16); + return result; +} + +static inline ovrVector3f fromGlm(const glm::vec3& v) { + return { v.x, v.y, v.z }; +} + +static inline ovrVector2f fromGlm(const glm::vec2& v) { + return { v.x, v.y }; +} + +static inline ovrQuatf fromGlm(const glm::quat& q) { + return { q.x, q.y, q.z, q.w }; +} + +static inline ovrPosef poseFromGlm(const glm::mat4& m) { + glm::vec3 translation = glm::vec3(m[3]) / m[3].w; + glm::quat orientation = glm::quat_cast(m); + ovrPosef result; + result.Orientation = fromGlm(orientation); + result.Position = fromGlm(translation); + return result; +} + +} + + + + + diff --git a/libraries/oculusMobile/src/ovr/TaskQueue.cpp b/libraries/oculusMobile/src/ovr/TaskQueue.cpp new file mode 100644 index 0000000000..5506a35acd --- /dev/null +++ b/libraries/oculusMobile/src/ovr/TaskQueue.cpp @@ -0,0 +1,40 @@ +// +// Created by Bradley Austin Davis on 2018/11/23 +// Copyright 2013-2018 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 "TaskQueue.h" + +using namespace ovr; + +void TaskQueue::submitTaskBlocking(Lock& lock, const Task& newTask) { + _task = newTask; + _taskPending = true; + _taskCondition.wait(lock, [=]() -> bool { return !_taskPending; }); +} + +void TaskQueue::submitTaskBlocking(const Task& task) { + Lock lock(_mutex); + submitTaskBlocking(lock, task); +} + +void TaskQueue::pollTask() { + Lock lock(_mutex); + if (_taskPending) { + _task(); + _taskPending = false; + _taskCondition.notify_one(); + } +} + +void TaskQueue::withLock(const Task& task) { + Lock lock(_mutex); + task(); +} + +void TaskQueue::withLockConditional(const LockTask& task) { + Lock lock(_mutex); + task(lock); +} diff --git a/libraries/oculusMobile/src/ovr/TaskQueue.h b/libraries/oculusMobile/src/ovr/TaskQueue.h new file mode 100644 index 0000000000..4ec055ece9 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/TaskQueue.h @@ -0,0 +1,42 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 +// +#pragma once + +#include +#include + +namespace ovr { + +using Mutex = std::mutex; +using Condition = std::condition_variable; +using Lock = std::unique_lock; +using Task = std::function; +using LockTask = std::function; + +class TaskQueue { +public: + // Execute a task on another thread + void submitTaskBlocking(const Task& task); + void submitTaskBlocking(Lock& lock, const Task& task); + void pollTask(); + + void withLock(const Task& task); + void withLockConditional(const LockTask& task); +private: + Mutex _mutex; + Task _task; + bool _taskPending{ false }; + Condition _taskCondition; +}; + +} + + + + + diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp new file mode 100644 index 0000000000..de2b4e1ff6 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -0,0 +1,337 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 "VrHandler.h" + +#include +#include + +#include + +#include +#include +#include +//#include + +#include "GLContext.h" +#include "Helpers.h" +#include "Framebuffer.h" + + + +using namespace ovr; + +static thread_local bool isRenderThread { false }; + +struct VrSurface : public TaskQueue { + using HandlerTask = VrHandler::HandlerTask; + + JavaVM* vm{nullptr}; + jobject oculusActivity{ nullptr }; + ANativeWindow* nativeWindow{ nullptr }; + + VrHandler* handler{nullptr}; + ovrMobile* session{nullptr}; + bool resumed { false }; + GLContext vrglContext; + Framebuffer eyeFbos[2]; + uint32_t readFbo{0}; + std::atomic presentIndex{1}; + double displayTime{0}; + + static constexpr float EYE_BUFFER_SCALE = 1.0f; + + void onCreate(JNIEnv* env, jobject activity) { + env->GetJavaVM(&vm); + oculusActivity = env->NewGlobalRef(activity); + } + + void setResumed(bool newResumed) { + this->resumed = newResumed; + submitRenderThreadTask([this](VrHandler* handler){ updateVrMode(); }); + } + + void setNativeWindow(ANativeWindow* newNativeWindow) { + auto oldNativeWindow = nativeWindow; + nativeWindow = newNativeWindow; + if (oldNativeWindow) { + ANativeWindow_release(oldNativeWindow); + } + submitRenderThreadTask([this](VrHandler* handler){ updateVrMode(); }); + } + + void init() { + if (!handler) { + return; + } + + EGLContext currentContext = eglGetCurrentContext(); + EGLDisplay currentDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + vrglContext.create(currentDisplay, currentContext); + vrglContext.makeCurrent(); + + glm::uvec2 eyeTargetSize; + withEnv([&](JNIEnv* env){ + ovrJava java{ vm, env, oculusActivity }; + eyeTargetSize = glm::uvec2 { + vrapi_GetSystemPropertyInt(&java, VRAPI_SYS_PROP_SUGGESTED_EYE_TEXTURE_WIDTH) * EYE_BUFFER_SCALE, + vrapi_GetSystemPropertyInt(&java, VRAPI_SYS_PROP_SUGGESTED_EYE_TEXTURE_HEIGHT) * EYE_BUFFER_SCALE, + }; + }); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "QQQ Eye Size %d, %d", eyeTargetSize.x, eyeTargetSize.y); + ovr::for_each_eye([&](ovrEye eye) { + eyeFbos[eye].create(eyeTargetSize); + }); + glGenFramebuffers(1, &readFbo); + vrglContext.doneCurrent(); + } + + void shutdown() { + } + + void setHandler(VrHandler *newHandler) { + withLock([&] { + isRenderThread = newHandler != nullptr; + if (handler != newHandler) { + shutdown(); + handler = newHandler; + init(); + if (handler) { + updateVrMode(); + } + } + }); + } + + void submitRenderThreadTask(const HandlerTask &task) { + withLockConditional([&](Lock &lock) { + if (handler != nullptr) { + submitTaskBlocking(lock, [&] { + task(handler); + }); + } + }); + } + + void withEnv(const std::function& f) { + JNIEnv* env = nullptr; + bool attached = false; + vm->GetEnv((void**)&env, JNI_VERSION_1_6); + if (!env) { + attached = true; + vm->AttachCurrentThread(&env, nullptr); + } + f(env); + if (attached) { + vm->DetachCurrentThread(); + } + } + + void updateVrMode() { + // For VR mode to be valid, the activity must be between an onResume and + // an onPause call and must additionally have a valid native window handle + bool vrReady = resumed && nullptr != nativeWindow; + // If we're IN VR mode, we'll have a non-null ovrMobile pointer in session + bool vrRunning = session != nullptr; + if (vrReady != vrRunning) { + if (vrRunning) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_LeaveVrMode"); + vrapi_LeaveVrMode(session); + session = nullptr; + oculusActivity = nullptr; + } else { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_EnterVrMode"); + withEnv([&](JNIEnv* env){ + ovrJava java{ vm, env, oculusActivity }; + ovrModeParms modeParms = vrapi_DefaultModeParms(&java); + modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + modeParms.Display = (unsigned long long) vrglContext.display; + modeParms.ShareContext = (unsigned long long) vrglContext.context; + modeParms.WindowSurface = (unsigned long long) nativeWindow; + session = vrapi_EnterVrMode(&modeParms); + ovrPosef trackingTransform = vrapi_GetTrackingTransform( session, VRAPI_TRACKING_TRANSFORM_SYSTEM_CENTER_EYE_LEVEL); + vrapi_SetTrackingTransform( session, trackingTransform ); + vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, pthread_self()); + vrapi_SetClockLevels(session, 2, 4); + vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_DYNAMIC); + vrapi_SetDisplayRefreshRate(session, 72); + }); + } + } + } + + void presentFrame(uint32_t sourceTexture, const glm::uvec2 &sourceSize, const ovrTracking2& tracking) { + ovrLayerProjection2 layer = vrapi_DefaultLayerProjection2(); + layer.HeadPose = tracking.HeadPose; + if (sourceTexture) { + glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); + glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, sourceTexture, 0); + GLenum framebufferStatus = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); + if (GL_FRAMEBUFFER_COMPLETE != framebufferStatus) { + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "incomplete framebuffer"); + } + } + GLenum invalidateAttachment = GL_COLOR_ATTACHMENT0; + + ovr::for_each_eye([&](ovrEye eye) { + const auto &eyeTracking = tracking.Eye[eye]; + auto &eyeFbo = eyeFbos[eye]; + const auto &destSize = eyeFbo._size; + eyeFbo.bind(); + glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, 1, &invalidateAttachment); + if (sourceTexture) { + auto sourceWidth = sourceSize.x / 2; + auto sourceX = (eye == VRAPI_EYE_LEFT) ? 0 : sourceWidth; + glBlitFramebuffer( + sourceX, 0, sourceX + sourceWidth, sourceSize.y, + 0, 0, destSize.x, destSize.y, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + eyeFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); + eyeFbo.advance(); + }); + if (sourceTexture) { + glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, &invalidateAttachment); + glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0); + } + glFlush(); + + ovrLayerHeader2 *layerHeader = &layer.Header; + ovrSubmitFrameDescription2 frameDesc = {}; + frameDesc.SwapInterval = 2; + frameDesc.FrameIndex = presentIndex; + frameDesc.DisplayTime = displayTime; + frameDesc.LayerCount = 1; + frameDesc.Layers = &layerHeader; + vrapi_SubmitFrame2(session, &frameDesc); + ++presentIndex; + } + + ovrTracking2 beginFrame() { + displayTime = vrapi_GetPredictedDisplayTime(session, presentIndex); + return vrapi_GetPredictedTracking2(session, displayTime); + } +}; + +static VrSurface SURFACE; + +bool VrHandler::vrActive() const { + return SURFACE.session != nullptr; +} + +void VrHandler::setHandler(VrHandler* handler) { + SURFACE.setHandler(handler); +} + +void VrHandler::pollTask() { + SURFACE.pollTask(); +} + +void VrHandler::makeCurrent() { + if (!SURFACE.vrglContext.makeCurrent()) { + __android_log_write(ANDROID_LOG_WARN, "QQQ", "Failed to make GL current"); + } +} + +void VrHandler::doneCurrent() { + SURFACE.vrglContext.doneCurrent(); +} + +uint32_t VrHandler::currentPresentIndex() const { + return SURFACE.presentIndex; +} + +ovrTracking2 VrHandler::beginFrame() { + return SURFACE.beginFrame(); +} + +void VrHandler::presentFrame(uint32_t sourceTexture, const glm::uvec2 &sourceSize, const ovrTracking2& tracking) const { + SURFACE.presentFrame(sourceTexture, sourceSize, tracking); +} + +bool VrHandler::withOvrJava(const OvrJavaTask& task) { + SURFACE.withEnv([&](JNIEnv* env){ + ovrJava java{ SURFACE.vm, env, SURFACE.oculusActivity }; + task(&java); + }); + return true; +} + +bool VrHandler::withOvrMobile(const OvrMobileTask &task) { + auto sessionTask = [&]()->bool{ + if (!SURFACE.session) { + return false; + } + task(SURFACE.session); + return true; + }; + + if (isRenderThread) { + return sessionTask(); + } + + bool result = false; + SURFACE.withLock([&]{ + result = sessionTask(); + }); + return result; +} + + +void VrHandler::initVr(const char* appId) { + withOvrJava([&](const ovrJava* java){ + ovrInitParms initParms = vrapi_DefaultInitParms(java); + initParms.GraphicsAPI = VRAPI_GRAPHICS_API_OPENGL_ES_3; + if (vrapi_Initialize(&initParms) != VRAPI_INITIALIZE_SUCCESS) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Failed vrapi init"); + } + }); + + // if (appId) { + // auto platformInitResult = ovr_PlatformInitializeAndroid(appId, activity.object(), env); + // if (ovrPlatformInitialize_Success != platformInitResult) { + // __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Failed ovr platform init"); + // } + // } +} + +void VrHandler::shutdownVr() { + vrapi_Shutdown(); +} + +extern "C" { + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { + __android_log_write(ANDROID_LOG_WARN, "QQQ", "oculusMobile::JNI_OnLoad"); + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + SURFACE.onCreate(env, obj); +} + +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnDestroy(JNIEnv*, jclass) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); +} + +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnResume(JNIEnv*, jclass) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + SURFACE.setResumed(true); +} + +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnPause(JNIEnv*, jclass) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + SURFACE.setResumed(false); +} + +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnSurfaceChanged(JNIEnv* env, jclass, jobject surface) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + SURFACE.setNativeWindow(surface ? ANativeWindow_fromSurface( env, surface ) : nullptr); +} + +} // extern "C" diff --git a/libraries/oculusMobile/src/ovr/VrHandler.h b/libraries/oculusMobile/src/ovr/VrHandler.h new file mode 100644 index 0000000000..3c36ee8626 --- /dev/null +++ b/libraries/oculusMobile/src/ovr/VrHandler.h @@ -0,0 +1,47 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 +// +#pragma once + +#include +#include +#include + +#include "TaskQueue.h" + +typedef struct ovrMobile ovrMobile; +namespace ovr { + +class VrHandler { +public: + using HandlerTask = std::function; + using OvrMobileTask = std::function; + using OvrJavaTask = std::function; + static void setHandler(VrHandler* handler); + static bool withOvrMobile(const OvrMobileTask& task); + +protected: + static void initVr(const char* appId = nullptr); + static void shutdownVr(); + static bool withOvrJava(const OvrJavaTask& task); + + uint32_t currentPresentIndex() const; + ovrTracking2 beginFrame(); + void presentFrame(uint32_t textureId, const glm::uvec2& size, const ovrTracking2& tracking) const; + + bool vrActive() const; + void pollTask(); + void makeCurrent(); + void doneCurrent(); +}; + +} + + + + + diff --git a/libraries/oculusMobilePlugin/CMakeLists.txt b/libraries/oculusMobilePlugin/CMakeLists.txt new file mode 100644 index 0000000000..b404c51f02 --- /dev/null +++ b/libraries/oculusMobilePlugin/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# Created by Bradley Austin Davis on 2018/11/15 +# Copyright 2013-2018 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 +# + +if (ANDROID) + set(TARGET_NAME oculusMobilePlugin) + setup_hifi_library(AndroidExtras Multimedia) + + # if we were passed an Oculus App ID for entitlement checks, send that along + if (DEFINED ENV{OCULUS_APP_ID}) + target_compile_definitions(${TARGET_NAME} -DOCULUS_APP_ID="$ENV{OCULUS_APP_ID}") + endif () + + link_hifi_libraries( + shared task gl shaders gpu controllers ui qml + plugins ui-plugins display-plugins input-plugins + audio-client networking render-utils + render graphics + oculusMobile + ${PLATFORM_GL_BACKEND} + ) + + include_hifi_library_headers(octree) + target_oculus_mobile() +endif() diff --git a/libraries/oculusMobilePlugin/src/Logging.cpp b/libraries/oculusMobilePlugin/src/Logging.cpp new file mode 100644 index 0000000000..3d8628b0cb --- /dev/null +++ b/libraries/oculusMobilePlugin/src/Logging.cpp @@ -0,0 +1,4 @@ +#include "Logging.h" + +Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") +Q_LOGGING_CATEGORY(oculusLog, "hifi.plugins.display.oculus") diff --git a/libraries/oculusMobilePlugin/src/Logging.h b/libraries/oculusMobilePlugin/src/Logging.h new file mode 100644 index 0000000000..066712ef6a --- /dev/null +++ b/libraries/oculusMobilePlugin/src/Logging.h @@ -0,0 +1,13 @@ +// +// Created by Bradley Austin Davis on 2018/11/20 +// Copyright 2013-2018 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 +// +#pragma once + +#include + +Q_DECLARE_LOGGING_CATEGORY(displayplugins) +Q_DECLARE_LOGGING_CATEGORY(oculusLog) diff --git a/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.cpp b/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.cpp new file mode 100644 index 0000000000..8de563ee4c --- /dev/null +++ b/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.cpp @@ -0,0 +1,694 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 "OculusMobileControllerManager.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "Logging.h" +#include + +const char* OculusMobileControllerManager::NAME = "Oculus"; +const quint64 LOST_TRACKING_DELAY = 3000000; + +namespace ovr { + + controller::Pose toControllerPose(ovrHandedness hand, const ovrRigidBodyPosef& handPose) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = + glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, -CONTROLLER_LENGTH_OFFSET / 2.0f, CONTROLLER_LENGTH_OFFSET * 1.5f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == VRAPI_HAND_LEFT ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == VRAPI_HAND_LEFT ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.Pose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(handPose.Pose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = rotation * toGlm(handPose.AngularVelocity); + pose.velocity = toGlm(handPose.LinearVelocity); + pose.valid = true; + return pose; + } + + controller::Pose toControllerPose(ovrHandedness hand, + const ovrRigidBodyPosef& handPose, + const ovrRigidBodyPosef& lastHandPose) { + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = + glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, -CONTROLLER_LENGTH_OFFSET / 2.0f, CONTROLLER_LENGTH_OFFSET * 1.5f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == VRAPI_HAND_LEFT ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == VRAPI_HAND_LEFT ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.Pose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(lastHandPose.Pose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = toGlm(lastHandPose.AngularVelocity); + pose.velocity = toGlm(lastHandPose.LinearVelocity); + pose.valid = true; + return pose; + } + +} + + +class OculusMobileInputDevice : public controller::InputDevice { + friend class OculusMobileControllerManager; +public: + using Pointer = std::shared_ptr; + static Pointer check(ovrMobile* session); + + OculusMobileInputDevice(ovrMobile* session, const std::vector& devicesCaps); + void updateHands(ovrMobile* session); + + controller::Input::NamedVector getAvailableInputs() const override; + QString getDefaultMappingConfig() const override; + void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + void focusOutEvent() override; + bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override; + +private: + void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, + ovrHandedness hand, const ovrRigidBodyPosef& handPose); + void handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, + ovrHandedness hand, const ovrRigidBodyPosef& handPose); + void handleHeadPose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, + const ovrRigidBodyPosef& headPose); + + // perform an action when the TouchDevice mutex is acquired. + using Locker = std::unique_lock; + + template + void withLock(F&& f) { Locker locker(_lock); f(); } + + mutable std::recursive_mutex _lock; + ovrTracking2 _headTracking; + struct HandData { + HandData() { + state.Header.ControllerType =ovrControllerType_TrackedRemote; + } + + float hapticDuration { 0.0f }; + float hapticStrength { 0.0f }; + bool valid{ false }; + bool lostTracking{ false }; + quint64 regainTrackingDeadline; + ovrRigidBodyPosef lastPose; + ovrInputTrackedRemoteCapabilities caps; + ovrInputStateTrackedRemote state; + ovrResult stateResult{ ovrError_NotInitialized }; + ovrTracking tracking; + ovrResult trackingResult{ ovrError_NotInitialized }; + bool setHapticFeedback(float strength, float duration) { +#if 0 + bool success = true; + bool sessionSuccess = ovr::VrHandler::withOvrMobile([&](ovrMobile* session){ + if (strength == 0.0f) { + hapticStrength = 0.0f; + hapticDuration = 0.0f; + } else { + hapticStrength = (duration > hapticDuration) ? strength : hapticStrength; + if (vrapi_SetHapticVibrationSimple(session, caps.Header.DeviceID, hapticStrength) != ovrSuccess) { + success = false; + } + hapticDuration = std::max(duration, hapticDuration); + } + }); + return success && sessionSuccess; +#else + return true; +#endif + } + + void stopHapticPulse() { + ovr::VrHandler::withOvrMobile([&](ovrMobile* session){ + vrapi_SetHapticVibrationSimple(session, caps.Header.DeviceID, 0.0f); + }); + } + + bool isValid() const { + return (stateResult == ovrSuccess) && (trackingResult == ovrSuccess); + } + + void update(ovrMobile* session, double time = 0.0) { + const auto& deviceId = caps.Header.DeviceID; + stateResult = vrapi_GetCurrentInputState(session, deviceId, &state.Header); + trackingResult = vrapi_GetInputTrackingState(session, deviceId, 0.0, &tracking); + } + }; + std::array _hands; +}; + +OculusMobileInputDevice::Pointer OculusMobileInputDevice::check(ovrMobile *session) { + Pointer result; + + std::vector devicesCaps; + { + uint32_t deviceIndex { 0 }; + ovrInputCapabilityHeader capsHeader; + while (vrapi_EnumerateInputDevices(session, deviceIndex, &capsHeader) >= 0) { + if (capsHeader.Type == ovrControllerType_TrackedRemote) { + ovrInputTrackedRemoteCapabilities caps; + caps.Header = capsHeader; + vrapi_GetInputDeviceCapabilities(session, &caps.Header); + devicesCaps.push_back(caps); + } + ++deviceIndex; + } + } + if (!devicesCaps.empty()) { + result.reset(new OculusMobileInputDevice(session, devicesCaps)); + } + return result; +} + +static OculusMobileInputDevice::Pointer oculusMobileControllers; + +bool OculusMobileControllerManager::isHandController() const { + return oculusMobileControllers.operator bool(); +} + +bool OculusMobileControllerManager::isSupported() const { + return true; +} + +bool OculusMobileControllerManager::activate() { + InputPlugin::activate(); + checkForConnectedDevices(); + return true; +} + +void OculusMobileControllerManager::checkForConnectedDevices() { + if (oculusMobileControllers) { + return; + } + + ovr::VrHandler::withOvrMobile([&](ovrMobile* session){ + oculusMobileControllers = OculusMobileInputDevice::check(session); + if (oculusMobileControllers) { + auto userInputMapper = DependencyManager::get(); + userInputMapper->registerDevice(oculusMobileControllers); + } + }); +} + +void OculusMobileControllerManager::deactivate() { + InputPlugin::deactivate(); + + // unregister with UserInputMapper + auto userInputMapper = DependencyManager::get(); + if (oculusMobileControllers) { + userInputMapper->removeDevice(oculusMobileControllers->getDeviceID()); + oculusMobileControllers.reset(); + } +} + +void OculusMobileControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + PerformanceTimer perfTimer("OculusMobileInputDevice::update"); + + checkForConnectedDevices(); + + if (!oculusMobileControllers) { + return; + } + + bool updated = ovr::VrHandler::withOvrMobile([&](ovrMobile* session){ + oculusMobileControllers->updateHands(session); + }); + + if (updated) { + oculusMobileControllers->update(deltaTime, inputCalibrationData); + } +} + +void OculusMobileControllerManager::pluginFocusOutEvent() { + if (oculusMobileControllers) { + oculusMobileControllers->focusOutEvent(); + } +} + +QStringList OculusMobileControllerManager::getSubdeviceNames() { + QStringList devices; + if (oculusMobileControllers) { + devices << oculusMobileControllers->getName(); + } + return devices; +} + +using namespace controller; + +static const std::vector> BUTTON_MAP { { + { ovrButton_Up, DU }, + { ovrButton_Down, DD }, + { ovrButton_Left, DL }, + { ovrButton_Right, DR }, + { ovrButton_Enter, START }, + { ovrButton_Back, BACK }, + { ovrButton_X, X }, + { ovrButton_Y, Y }, + { ovrButton_A, A }, + { ovrButton_B, B }, + { ovrButton_LThumb, LS }, + { ovrButton_RThumb, RS }, + //{ ovrButton_LShoulder, LB }, + //{ ovrButton_RShoulder, RB }, +} }; + +static const std::vector> LEFT_TOUCH_MAP { { + { ovrTouch_X, LEFT_PRIMARY_THUMB_TOUCH }, + { ovrTouch_Y, LEFT_SECONDARY_THUMB_TOUCH }, + { ovrTouch_LThumb, LS_TOUCH }, + { ovrTouch_ThumbUp, LEFT_THUMB_UP }, + { ovrTouch_IndexTrigger, LEFT_PRIMARY_INDEX_TOUCH }, + { ovrTouch_IndexPointing, LEFT_INDEX_POINT }, +} }; + + +static const std::vector> RIGHT_TOUCH_MAP { { + { ovrTouch_A, RIGHT_PRIMARY_THUMB_TOUCH }, + { ovrTouch_B, RIGHT_SECONDARY_THUMB_TOUCH }, + { ovrTouch_RThumb, RS_TOUCH }, + { ovrTouch_ThumbUp, RIGHT_THUMB_UP }, + { ovrTouch_IndexTrigger, RIGHT_PRIMARY_INDEX_TOUCH }, + { ovrTouch_IndexPointing, RIGHT_INDEX_POINT }, +} }; + +void OculusMobileInputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + _buttonPressedMap.clear(); + + int numTrackedControllers = 0; + quint64 currentTime = usecTimestampNow(); + handleHeadPose(deltaTime, inputCalibrationData, _headTracking.HeadPose); + + static const auto REQUIRED_HAND_STATUS = VRAPI_TRACKING_STATUS_ORIENTATION_TRACKED | VRAPI_TRACKING_STATUS_POSITION_TRACKED; + ovr::for_each_hand([&](ovrHandedness hand) { + size_t handIndex = (hand == VRAPI_HAND_LEFT) ? 0 : 1; + int controller = (hand == VRAPI_HAND_LEFT) ? controller::LEFT_HAND : controller::RIGHT_HAND; + auto& handData = _hands[handIndex]; + const auto& tracking = handData.tracking; + ++numTrackedControllers; + + // Disable hand tracking while in Oculus Dash (Dash renders it's own hands) +// if (!hasInputFocus) { +// _poseStateMap.erase(controller); +// _poseStateMap[controller].valid = false; +// return; +// } + + if (REQUIRED_HAND_STATUS == (tracking.Status & REQUIRED_HAND_STATUS)) { + _poseStateMap.erase(controller); + handlePose(deltaTime, inputCalibrationData, hand, tracking.HeadPose); + handData.lostTracking = false; + handData.lastPose = tracking.HeadPose; + return; + } + + if (handData.lostTracking) { + if (currentTime > handData.regainTrackingDeadline) { + _poseStateMap.erase(controller); + _poseStateMap[controller].valid = false; + return; + } + } else { + quint64 deadlineToRegainTracking = currentTime + LOST_TRACKING_DELAY; + handData.regainTrackingDeadline = deadlineToRegainTracking; + handData.lostTracking = true; + } + handleRotationForUntrackedHand(inputCalibrationData, hand, tracking.HeadPose); + }); + + + using namespace controller; + // Axes + { + const auto& inputState = _hands[0].state; + _axisStateMap[LX].value = inputState.JoystickNoDeadZone.x; + _axisStateMap[LY].value = inputState.JoystickNoDeadZone.y; + _axisStateMap[LT].value = inputState.IndexTrigger; + _axisStateMap[LEFT_GRIP].value = inputState.GripTrigger; + for (const auto& pair : BUTTON_MAP) { + if (inputState.Buttons & pair.first) { + _buttonPressedMap.insert(pair.second); + qDebug()<<"AAAA:BUTTON PRESSED "<The Controller.Hardware.OculusTouch object has properties representing Oculus Rift. The property values are + * integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + * Controller.Standard items in a {@link RouteObject} mapping.

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyTypeDataDescription
Buttons
Anumbernumber"A" button pressed.
Bnumbernumber"B" button pressed.
Xnumbernumber"X" button pressed.
Ynumbernumber"Y" button pressed.
LeftApplicationMenunumbernumberLeft application menu button pressed. + *
RightApplicationMenunumbernumberRight application menu button pressed. + *
Sticks
LXnumbernumberLeft stick x-axis scale.
LYnumbernumberLeft stick y-axis scale.
RXnumbernumberRight stick x-axis scale.
RYnumbernumberRight stick y-axis scale.
LSnumbernumberLeft stick button pressed.
RSnumbernumberRight stick button pressed.
LSTouchnumbernumberLeft stick is touched.
RSTouchnumbernumberRight stick is touched.
Triggers
LTnumbernumberLeft trigger scale.
RTnumbernumberRight trigger scale.
LeftGripnumbernumberLeft grip scale.
RightGripnumbernumberRight grip scale.
Finger Abstractions
LeftPrimaryThumbTouchnumbernumberLeft thumb touching primary thumb + * button.
LeftSecondaryThumbTouchnumbernumberLeft thumb touching secondary thumb + * button.
LeftThumbUpnumbernumberLeft thumb not touching primary or secondary + * thumb buttons.
RightPrimaryThumbTouchnumbernumberRight thumb touching primary thumb + * button.
RightSecondaryThumbTouchnumbernumberRight thumb touching secondary thumb + * button.
RightThumbUpnumbernumberRight thumb not touching primary or secondary + * thumb buttons.
LeftPrimaryIndexTouchnumbernumberLeft index finger is touching primary + * index finger control.
LeftIndexPointnumbernumberLeft index finger is pointing, not touching + * primary or secondary index finger controls.
RightPrimaryIndexTouchnumbernumberRight index finger is touching primary + * index finger control.
RightIndexPointnumbernumberRight index finger is pointing, not touching + * primary or secondary index finger controls.
Avatar Skeleton
Headnumber{@link Pose}Head pose.
LeftHandnumber{@link Pose}Left hand pose.
RightHandnumber{@link Pose}right hand pose.
+ * @typedef {object} Controller.Hardware-OculusTouch + */ +controller::Input::NamedVector OculusMobileInputDevice::getAvailableInputs() const { + using namespace controller; + QVector availableInputs{ + // buttons + makePair(A, "A"), + makePair(B, "B"), + makePair(X, "X"), + makePair(Y, "Y"), + + // trackpad analogs + makePair(LX, "LX"), + makePair(LY, "LY"), + makePair(RX, "RX"), + makePair(RY, "RY"), + + // triggers + makePair(LT, "LT"), + makePair(RT, "RT"), + + // trigger buttons + //makePair(LB, "LB"), + //makePair(RB, "RB"), + + // side grip triggers + makePair(LEFT_GRIP, "LeftGrip"), + makePair(RIGHT_GRIP, "RightGrip"), + + // joystick buttons + makePair(LS, "LS"), + makePair(RS, "RS"), + + makePair(LEFT_HAND, "LeftHand"), + makePair(RIGHT_HAND, "RightHand"), + makePair(HEAD, "Head"), + + makePair(LEFT_PRIMARY_THUMB_TOUCH, "LeftPrimaryThumbTouch"), + makePair(LEFT_SECONDARY_THUMB_TOUCH, "LeftSecondaryThumbTouch"), + makePair(RIGHT_PRIMARY_THUMB_TOUCH, "RightPrimaryThumbTouch"), + makePair(RIGHT_SECONDARY_THUMB_TOUCH, "RightSecondaryThumbTouch"), + makePair(LEFT_PRIMARY_INDEX_TOUCH, "LeftPrimaryIndexTouch"), + makePair(RIGHT_PRIMARY_INDEX_TOUCH, "RightPrimaryIndexTouch"), + makePair(LS_TOUCH, "LSTouch"), + makePair(RS_TOUCH, "RSTouch"), + makePair(LEFT_THUMB_UP, "LeftThumbUp"), + makePair(RIGHT_THUMB_UP, "RightThumbUp"), + makePair(LEFT_INDEX_POINT, "LeftIndexPoint"), + makePair(RIGHT_INDEX_POINT, "RightIndexPoint"), + + makePair(BACK, "LeftApplicationMenu"), + makePair(START, "RightApplicationMenu"), + }; + return availableInputs; +} + +OculusMobileInputDevice::OculusMobileInputDevice(ovrMobile* session, const std::vector& devicesCaps) : controller::InputDevice("OculusTouch") { + qWarning() << "QQQ" << __FUNCTION__ << "Found " << devicesCaps.size() << "devices"; + for (const auto& deviceCaps : devicesCaps) { + size_t handIndex = -1; + if (deviceCaps.ControllerCapabilities & ovrControllerCaps_LeftHand) { + handIndex = 0; + } else if (deviceCaps.ControllerCapabilities & ovrControllerCaps_RightHand) { + handIndex = 1; + } else { + continue; + } + HandData& handData = _hands[handIndex]; + handData.state.Header.ControllerType = ovrControllerType_TrackedRemote; + handData.valid = true; + handData.caps = deviceCaps; + handData.update(session); + } +} + +void OculusMobileInputDevice::updateHands(ovrMobile* session) { + _headTracking = vrapi_GetPredictedTracking2(session, 0.0); + for (auto& hand : _hands) { + hand.update(session); + } +} + +QString OculusMobileInputDevice::getDefaultMappingConfig() const { + static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/oculus_touch.json"; + return MAPPING_JSON; +} + +// TODO migrate to a DLL model where plugins are discovered and loaded at runtime by the PluginManager class +InputPluginList getInputPlugins() { + InputPlugin* PLUGIN_POOL[] = { + new KeyboardMouseDevice(), + new OculusMobileControllerManager(), + nullptr + }; + + InputPluginList result; + for (int i = 0; PLUGIN_POOL[i]; ++i) { + InputPlugin* plugin = PLUGIN_POOL[i]; + if (plugin->isSupported()) { + result.push_back(InputPluginPointer(plugin)); + } + } + return result; +} \ No newline at end of file diff --git a/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.h b/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.h new file mode 100644 index 0000000000..43ead8d6a2 --- /dev/null +++ b/libraries/oculusMobilePlugin/src/OculusMobileControllerManager.h @@ -0,0 +1,43 @@ +// +// Created by Bradley Austin Davis on 2016/03/04 +// Copyright 2013-2016 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__OculusMobileControllerManager +#define hifi__OculusMobileControllerManager + +#include +#include +#include + +#include + +#include +#include + +class OculusMobileControllerManager : public InputPlugin { +Q_OBJECT +public: + // Plugin functions + bool isSupported() const override; + const QString getName() const override { return NAME; } + bool isHandController() const override; + bool isHeadController() const override { return true; } + QStringList getSubdeviceNames() override; + + bool activate() override; + void deactivate() override; + + void pluginFocusOutEvent() override; + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + +private: + static const char* NAME; + + void checkForConnectedDevices(); +}; + +#endif // hifi__OculusMobileControllerManager diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp new file mode 100644 index 0000000000..34ba130c71 --- /dev/null +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -0,0 +1,269 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 "OculusMobileDisplayPlugin.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace ovr; + +const char* OculusMobileDisplayPlugin::NAME { "Oculus Rift" }; +//thread_local bool renderThread = false; +#define OCULUS_APP_ID 2331695256865113 + +OculusMobileDisplayPlugin::OculusMobileDisplayPlugin() { + +} + +OculusMobileDisplayPlugin::~OculusMobileDisplayPlugin() { +} + +void OculusMobileDisplayPlugin::init() { + Parent::init(); + initVr(); + + emit deviceConnected(getName()); +} + +void OculusMobileDisplayPlugin::deinit() { + shutdownVr(); + Parent::deinit(); +} + +bool OculusMobileDisplayPlugin::internalActivate() { + _renderTargetSize = { 1024, 512 }; + _cullingProjection = ovr::toGlm(ovrMatrix4f_CreateProjectionFov(90.0f, 90.0f, 0.0f, 0.0f, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + + + withOvrJava([&](const ovrJava* java){ + _renderTargetSize = glm::uvec2{ + vrapi_GetSystemPropertyInt(java, VRAPI_SYS_PROP_SUGGESTED_EYE_TEXTURE_WIDTH), + vrapi_GetSystemPropertyInt(java, VRAPI_SYS_PROP_SUGGESTED_EYE_TEXTURE_HEIGHT), + }; + }); + + ovr::for_each_eye([&](ovrEye eye){ + _eyeProjections[eye] = _cullingProjection; + }); + + // This must come after the initialization, so that the values calculated + // above are available during the customizeContext call (when not running + // in threaded present mode) + return Parent::internalActivate(); +} + +void OculusMobileDisplayPlugin::internalDeactivate() { + Parent::internalDeactivate(); + // ovr::releaseRenderSession(_session); +} + +void OculusMobileDisplayPlugin::customizeContext() { + qWarning() << "QQQ" << __FUNCTION__ << "done"; + gl::initModuleGl(); + _mainContext = _container->getPrimaryWidget()->context(); + _mainContext->makeCurrent(); + ovr::VrHandler::setHandler(this); + _mainContext->doneCurrent(); + _mainContext->makeCurrent(); + Parent::customizeContext(); + qWarning() << "QQQ" << __FUNCTION__ << "done"; +} + +void OculusMobileDisplayPlugin::uncustomizeContext() { + ovr::VrHandler::setHandler(nullptr); + _mainContext->doneCurrent(); + _mainContext->makeCurrent(); + Parent::uncustomizeContext(); +} + +QRectF OculusMobileDisplayPlugin::getPlayAreaRect() { + QRectF result; + VrHandler::withOvrMobile([&](ovrMobile* session){ + ovrPosef pose; + ovrVector3f scale; + if (ovrSuccess != vrapi_GetBoundaryOrientedBoundingBox(session, &pose, &scale)) { + return; + } + // FIXME extract the center from the pose + glm::vec2 center { 0 }; + glm::vec2 dimensions = glm::vec2(scale.x, scale.z); + dimensions *= 2.0f; + result = QRectF(center.x, center.y, dimensions.x, dimensions.y); + }); + return result; +} + +glm::mat4 OculusMobileDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + glm::mat4 result = baseProjection; + VrHandler::withOvrMobile([&](ovrMobile* session){ + auto trackingState = vrapi_GetPredictedTracking2(session, 0.0); + result = ovr::Fov{ trackingState.Eye[eye].ProjectionMatrix }.withZ(baseProjection); + }); + return result; +} + +glm::mat4 OculusMobileDisplayPlugin::getCullingProjection(const glm::mat4& baseProjection) const { + glm::mat4 result = baseProjection; + VrHandler::withOvrMobile([&](ovrMobile* session){ + auto trackingState = vrapi_GetPredictedTracking2(session, 0.0); + ovr::Fov fovs[2]; + for (size_t i = 0; i < 2; ++i) { + fovs[i].extract(trackingState.Eye[i].ProjectionMatrix); + } + fovs[0].extend(fovs[1]); + return fovs[0].withZ(baseProjection); + }); + return result; +} + +void OculusMobileDisplayPlugin::resetSensors() { + VrHandler::withOvrMobile([&](ovrMobile* session){ + vrapi_RecenterPose(session); + }); + _currentRenderFrameInfo.renderPose = glm::mat4(); // identity +} + +float OculusMobileDisplayPlugin::getTargetFrameRate() const { + float result = 0.0f; + VrHandler::withOvrJava([&](const ovrJava* java){ + result = vrapi_GetSystemPropertyFloat(java, VRAPI_SYS_PROP_DISPLAY_REFRESH_RATE); + }); + return result; +} + +bool OculusMobileDisplayPlugin::isHmdMounted() const { + bool result = false; + VrHandler::withOvrJava([&](const ovrJava* java){ + result = VRAPI_FALSE != vrapi_GetSystemStatusInt(java, VRAPI_SYS_STATUS_MOUNTED); + }); + return result; +} + +static void goToDevMobile() { + auto addressManager = DependencyManager::get(); + auto currentAddress = addressManager->currentAddress().toString().toStdString(); + if (std::string::npos == currentAddress.find("dev-mobile")) { + addressManager->handleLookupString("hifi://dev-mobile/495.236,501.017,482.434/0,0.97452,0,-0.224301"); + //addressManager->handleLookupString("hifi://dev-mobile/504,498,491/0,0,0,0"); + //addressManager->handleLookupString("hifi://dev-mobile/0,-1,1"); + } +} + +// Called on the render thread, establishes the rough tracking for the upcoming +bool OculusMobileDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + static QAndroidJniEnvironment* jniEnv = nullptr; + if (nullptr == jniEnv) { + jniEnv = new QAndroidJniEnvironment(); + } + bool result = false; + _currentRenderFrameInfo = FrameInfo(); + ovrTracking2 trackingState = {}; + static bool resetTrackingTransform = true; + static glm::mat4 transformOffset; + + VrHandler::withOvrMobile([&](ovrMobile* session){ + if (resetTrackingTransform) { + auto pose = vrapi_GetTrackingTransform( session, VRAPI_TRACKING_TRANSFORM_SYSTEM_CENTER_FLOOR_LEVEL); + transformOffset = glm::inverse(ovr::toGlm(pose)); + vrapi_SetTrackingTransform( session, pose); + resetTrackingTransform = false; + } + // Find a better way of + _currentRenderFrameInfo.predictedDisplayTime = vrapi_GetPredictedDisplayTime(session, currentPresentIndex() + 2); + trackingState = vrapi_GetPredictedTracking2(session, _currentRenderFrameInfo.predictedDisplayTime); + result = true; + }); + + + + if (result) { + _currentRenderFrameInfo.renderPose = transformOffset; + withNonPresentThreadLock([&] { + _currentRenderFrameInfo.sensorSampleTime = trackingState.HeadPose.TimeInSeconds; + _currentRenderFrameInfo.renderPose = transformOffset * ovr::toGlm(trackingState.HeadPose.Pose); + _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + _ipd = vrapi_GetInterpupillaryDistance(&trackingState); + ovr::for_each_eye([&](ovrEye eye){ + _eyeProjections[eye] = ovr::toGlm(trackingState.Eye[eye].ProjectionMatrix); + _eyeOffsets[eye] = glm::translate(mat4(), vec3{ _ipd * (eye == VRAPI_EYE_LEFT ? -0.5f : 0.5f), 0.0f, 0.0f }); + }); + }); + } + + // static uint32_t count = 0; + // if ((++count % 1000) == 0) { + // AbstractViewStateInterface::instance()->postLambdaEvent([] { + // goToDevMobile(); + // }); + // } + + return result && Parent::beginFrameRender(frameIndex); +} + +ovrTracking2 presentTracking; + +void OculusMobileDisplayPlugin::updatePresentPose() { + static QAndroidJniEnvironment* jniEnv = nullptr; + if (nullptr == jniEnv) { + jniEnv = new QAndroidJniEnvironment(); + } + VrHandler::withOvrMobile([&](ovrMobile* session){ + presentTracking = beginFrame(); + _currentPresentFrameInfo.sensorSampleTime = vrapi_GetTimeInSeconds(); + _currentPresentFrameInfo.predictedDisplayTime = presentTracking.HeadPose.TimeInSeconds; + _currentPresentFrameInfo.presentPose = ovr::toGlm(presentTracking.HeadPose.Pose); + }); +} + +void OculusMobileDisplayPlugin::internalPresent() { + VrHandler::pollTask(); + + if (!vrActive()) { + QThread::msleep(1); + return; + } + + auto sourceTexture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + glm::uvec2 sourceSize{ _compositeFramebuffer->getWidth(), _compositeFramebuffer->getHeight() }; + VrHandler::presentFrame(sourceTexture, sourceSize, presentTracking); + _presentRate.increment(); +} + +DisplayPluginList getDisplayPlugins() { + static DisplayPluginList result; + static std::once_flag once; + std::call_once(once, [&]{ + auto plugin = std::make_shared(); + plugin->isSupported(); + result.push_back(plugin); + }); + return result; +} + diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h new file mode 100644 index 0000000000..4a0a21e995 --- /dev/null +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -0,0 +1,65 @@ +// +// Created by Bradley Austin Davis on 2018/11/15 +// Copyright 2013-2018 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 +// +#pragma once + +#include + +#include + +#include +#include +#include + +#include +#include + +typedef struct ovrTextureSwapChain ovrTextureSwapChain; +typedef struct ovrMobile ovrMobile; +typedef struct ANativeWindow ANativeWindow; + +class OculusMobileDisplayPlugin : public HmdDisplayPlugin, public ovr::VrHandler { + using Parent = HmdDisplayPlugin; +public: + OculusMobileDisplayPlugin(); + virtual ~OculusMobileDisplayPlugin(); + bool isSupported() const override { return true; }; + bool hasAsyncReprojection() const override { return true; } + bool getSupportsAutoSwitch() override final { return false; } + QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; } + + glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const override; + glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const override; + + // Stereo specific methods + void resetSensors() override final; + bool beginFrameRender(uint32_t frameIndex) override; + + QRectF getPlayAreaRect() override; + float getTargetFrameRate() const override; + void init() override; + void deinit() override; + +protected: + const QString getName() const override { return NAME; } + + bool internalActivate() override; + void internalDeactivate() override; + + void customizeContext() override; + void uncustomizeContext() override; + + void updatePresentPose() override; + void internalPresent() override; + void hmdPresent() override { throw std::runtime_error("Unused"); } + bool isHmdMounted() const override; + + static const char* NAME; + mutable gl::Context* _mainContext{ nullptr }; + uint32_t _readFbo; +}; + From eb0566b94eeb47324302dc51237e831fcb0219db Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 13:21:38 -0800 Subject: [PATCH 052/130] Fix signing configs --- android/apps/framePlayer/build.gradle | 8 ++++---- android/apps/questFramePlayer/build.gradle | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/apps/framePlayer/build.gradle b/android/apps/framePlayer/build.gradle index fc8651fce1..a210c8300e 100644 --- a/android/apps/framePlayer/build.gradle +++ b/android/apps/framePlayer/build.gradle @@ -3,10 +3,10 @@ apply plugin: 'com.android.application' android { signingConfigs { release { - keyAlias 'key0' - keyPassword 'password' - storeFile file('C:/android/keystore.jks') - storePassword 'password' + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : '' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : '' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : '' } } diff --git a/android/apps/questFramePlayer/build.gradle b/android/apps/questFramePlayer/build.gradle index 13d806c3a4..899f9cb955 100644 --- a/android/apps/questFramePlayer/build.gradle +++ b/android/apps/questFramePlayer/build.gradle @@ -3,10 +3,10 @@ apply plugin: 'com.android.application' android { signingConfigs { release { - keyAlias 'key0' - keyPassword 'password' - storeFile file('C:/android/keystore.jks') - storePassword 'password' + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : '' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : '' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : '' } } From 3866c7568cb7b67d004b0102a0b1a32d68d247c5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 13:27:52 -0800 Subject: [PATCH 053/130] Update dockerfile --- android/docker/Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/android/docker/Dockerfile b/android/docker/Dockerfile index c37f73cb2a..fe3a83950a 100644 --- a/android/docker/Dockerfile +++ b/android/docker/Dockerfile @@ -73,13 +73,10 @@ RUN mkdir "$HIFI_BASE" && \ RUN git clone https://github.com/jherico/hifi.git && \ cd ~/hifi && \ - git checkout feature/quest_move_interface + git checkout feature/quest_frame_player WORKDIR /home/jenkins/hifi -RUN touch .test6 && \ - git fetch && git reset origin/feature/quest_move_interface --hard - RUN mkdir build # Pre-cache the vcpkg managed dependencies From 2db5bbd38103a19ca076c6116bea0ecf350479fc Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 30 Jan 2019 14:45:31 -0800 Subject: [PATCH 054/130] Fix mac frame player --- tools/gpu-frame-player/src/RenderThread.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/gpu-frame-player/src/RenderThread.cpp b/tools/gpu-frame-player/src/RenderThread.cpp index a9dcf8900f..ff0d7630e5 100644 --- a/tools/gpu-frame-player/src/RenderThread.cpp +++ b/tools/gpu-frame-player/src/RenderThread.cpp @@ -32,9 +32,12 @@ void RenderThread::initialize(QWindow* window) { _window = window; #ifdef USE_GL + _window->setFormat(getDefaultOpenGLSurfaceFormat()); _context.setWindow(window); _context.create(); - _context.makeCurrent(); + if (!_context.makeCurrent()) { + qFatal("Unable to make context current"); + } QOpenGLContextWrapper(_context.qglContext()).makeCurrent(_window); glGenTextures(1, &_externalTexture); glBindTexture(GL_TEXTURE_2D, _externalTexture); From 9714271a2a3080ef56813c8a1cd470d5b316ab19 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Jan 2019 15:22:13 -0800 Subject: [PATCH 055/130] Migrate shutdown hack applied to QtActivity to InterfaceActivity --- .../hifiinterface/InterfaceActivity.java | 22 ++++++++++++++++++- .../qt5/android/bindings/QtActivity.java | 20 +---------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index b7d2157737..6428044df0 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -31,6 +31,7 @@ import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.SlidingDrawer; +import org.qtproject.qt5.android.QtNative; import org.qtproject.qt5.android.QtLayout; import org.qtproject.qt5.android.QtSurface; import org.qtproject.qt5.android.bindings.QtActivity; @@ -166,8 +167,27 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW @Override protected void onDestroy() { - super.onDestroy(); nativeOnDestroy(); + /* + cduarte https://highfidelity.manuscript.com/f/cases/16712/App-freezes-on-opening-randomly + After Qt upgrade to 5.11 we had a black screen crash after closing the application with + the hardware button "Back" and trying to start the app again. It could only be fixed after + totally closing the app swiping it in the list of running apps. + This problem did not happen with the previous Qt version. + After analysing changes we came up with this case and change: + https://codereview.qt-project.org/#/c/218882/ + In summary they've moved libs loading to the same thread as main() and as a matter of correctness + in the onDestroy method in QtActivityDelegate, they exit that thread with `QtNative.m_qtThread.exit();` + That exit call is the main reason of this problem. + + In this fix we just replace the `QtApplication.invokeDelegate();` call that may end using the + entire onDestroy method including that thread exit line for other three lines that purposely + terminate qt (borrowed from QtActivityDelegate::onDestroy as well). + */ + QtNative.terminateQt(); + QtNative.setActivity(null, null); + System.exit(0); + super.onDestroy(); } @Override diff --git a/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java index 93ae2bc227..40e1863d69 100644 --- a/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java +++ b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java @@ -364,25 +364,7 @@ public class QtActivity extends Activity { @Override protected void onDestroy() { super.onDestroy(); - /* - cduarte https://highfidelity.manuscript.com/f/cases/16712/App-freezes-on-opening-randomly - After Qt upgrade to 5.11 we had a black screen crash after closing the application with - the hardware button "Back" and trying to start the app again. It could only be fixed after - totally closing the app swiping it in the list of running apps. - This problem did not happen with the previous Qt version. - After analysing changes we came up with this case and change: - https://codereview.qt-project.org/#/c/218882/ - In summary they've moved libs loading to the same thread as main() and as a matter of correctness - in the onDestroy method in QtActivityDelegate, they exit that thread with `QtNative.m_qtThread.exit();` - That exit call is the main reason of this problem. - - In this fix we just replace the `QtApplication.invokeDelegate();` call that may end using the - entire onDestroy method including that thread exit line for other three lines that purposely - terminate qt (borrowed from QtActivityDelegate::onDestroy as well). - */ - QtNative.terminateQt(); - QtNative.setActivity(null, null); - System.exit(0); + QtApplication.invokeDelegate(); } //--------------------------------------------------------------------------- From 6e61c02d04b65bcc32211b7b6e1667d5cab06bcd Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 17:17:39 -0800 Subject: [PATCH 056/130] fix getEntityProperties --- libraries/entities/src/EntityItem.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 41e4f43a5d..1fb1ebb1bc 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -88,13 +88,13 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_REGISTRATION_POINT; requestedProperties += PROP_CREATED; requestedProperties += PROP_LAST_EDITED_BY; - //requestedProperties += PROP_ENTITY_HOST_TYPE; // not sent over the wire - //requestedProperties += PROP_OWNING_AVATAR_ID; // not sent over the wire + requestedProperties += PROP_ENTITY_HOST_TYPE; + requestedProperties += PROP_OWNING_AVATAR_ID; requestedProperties += PROP_PARENT_ID; requestedProperties += PROP_PARENT_JOINT_INDEX; requestedProperties += PROP_QUERY_AA_CUBE; requestedProperties += PROP_CAN_CAST_SHADOW; - // requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; // not sent over the wire + requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; requestedProperties += PROP_RENDER_LAYER; requestedProperties += PROP_PRIMITIVE_MODE; requestedProperties += PROP_IGNORE_PICK_INTERSECTION; @@ -180,6 +180,11 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet EntityPropertyFlags propertyFlags(PROP_LAST_ITEM); EntityPropertyFlags requestedProperties = getEntityProperties(params); + // these properties are not sent over the wire + requestedProperties -= PROP_ENTITY_HOST_TYPE; + requestedProperties -= PROP_OWNING_AVATAR_ID; + requestedProperties -= PROP_VISIBLE_IN_SECONDARY_CAMERA; + // If we are being called for a subsequent pass at appendEntityData() that failed to completely encode this item, // then our entityTreeElementExtraEncodeData should include data about which properties we need to append. if (entityTreeElementExtraEncodeData && entityTreeElementExtraEncodeData->entities.contains(getEntityItemID())) { From 283dabc62296ce314257427e0f2cd181033e61a0 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 30 Jan 2019 17:03:16 -0800 Subject: [PATCH 057/130] Fix reload mechanic for entity server scripts --- .../src/scripts/EntityScriptServer.cpp | 16 ++++++++-------- .../src/scripts/EntityScriptServer.h | 2 +- .../entities-renderer/src/EntityTreeRenderer.cpp | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index ef0c807bc4..f1a6c97831 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -112,7 +112,6 @@ void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointerunloadEntityScript(entityID); checkAndCallPreload(entityID, true); } } @@ -184,7 +183,6 @@ void EntityScriptServer::updateEntityPPS() { pps = std::min(_maxEntityPPS, pps); } _entityEditSender.setPacketsPerSecond(pps); - qDebug() << QString("Updating entity PPS to: %1 @ %2 PPS per script = %3 PPS").arg(numRunningScripts).arg(_entityPPSPerScript).arg(pps); } void EntityScriptServer::handleEntityServerScriptLogPacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -525,23 +523,25 @@ void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); checkAndCallPreload(entityID, reload); } } -void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, bool reload) { +void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, bool forceRedownload) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { EntityItemPointer entity = _entityViewer.getTree()->findEntityByEntityItemID(entityID); EntityScriptDetails details; - bool notRunning = !_entitiesScriptEngine->getEntityScriptDetails(entityID, details); - if (entity && (reload || notRunning || details.scriptText != entity->getServerScripts())) { + bool isRunning = _entitiesScriptEngine->getEntityScriptDetails(entityID, details); + if (entity && (forceRedownload || !isRunning || details.scriptText != entity->getServerScripts())) { + if (isRunning) { + _entitiesScriptEngine->unloadEntityScript(entityID, true); + } + QString scriptUrl = entity->getServerScripts(); if (!scriptUrl.isEmpty()) { scriptUrl = DependencyManager::get()->normalizeURL(scriptUrl); - qCDebug(entity_script_server) << "Loading entity server script" << scriptUrl << "for" << entityID; - _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload); + _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, forceRedownload); } } } diff --git a/assignment-client/src/scripts/EntityScriptServer.h b/assignment-client/src/scripts/EntityScriptServer.h index 9c6c4c752e..944fee36a3 100644 --- a/assignment-client/src/scripts/EntityScriptServer.h +++ b/assignment-client/src/scripts/EntityScriptServer.h @@ -67,7 +67,7 @@ private: void addingEntity(const EntityItemID& entityID); void deletingEntity(const EntityItemID& entityID); void entityServerScriptChanging(const EntityItemID& entityID, bool reload); - void checkAndCallPreload(const EntityItemID& entityID, bool reload = false); + void checkAndCallPreload(const EntityItemID& entityID, bool forceRedownload = false); void cleanupOldKilledListeners(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c71b296a74..44025fc8f4 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1048,7 +1048,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool QString scriptUrl = entity->getScript(); if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) { if (_entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID); } entity->scriptHasUnloaded(); } From 5553752d811247dc245ba7c88057623198a56a29 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 18:04:44 -0800 Subject: [PATCH 058/130] fall when flying not allowed --- libraries/physics/src/CharacterController.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 13a1328065..27c074900f 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -672,7 +672,7 @@ void CharacterController::updateState() { return; } if (_pendingFlags & PENDING_FLAG_RECOMPUTE_FLYING) { - SET_STATE(CharacterController::State::Hover, "recomputeFlying"); + SET_STATE(CharacterController::State::Hover, "recomputeFlying"); _hasSupport = false; _stepHeight = _minStepHeight; // clears memory of last step obstacle _pendingFlags &= ~PENDING_FLAG_RECOMPUTE_FLYING; @@ -784,15 +784,13 @@ void CharacterController::updateState() { // Transition to hover if we are above the fall threshold SET_STATE(State::Hover, "above fall threshold"); } - } else if (!rayHasHit && !_hasSupport) { - SET_STATE(State::Hover, "no ground detected"); } break; } case State::Hover: btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); - if (!_flyingAllowed && rayHasHit) { + if (!_flyingAllowed) { SET_STATE(State::InAir, "flying not allowed"); } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); From 2d0c1184e476e865b6d12ae3c7f1de1584ad4daa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 30 Jan 2019 18:27:17 -0800 Subject: [PATCH 059/130] only domain zone entities control flying and ghosting --- interface/src/avatar/MyAvatar.cpp | 11 ++++------- .../entities-renderer/src/EntityTreeRenderer.cpp | 11 +++++++++++ libraries/entities-renderer/src/EntityTreeRenderer.h | 2 +- libraries/entities/src/EntityItemProperties.cpp | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 92d9270d20..8c22d8101a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -915,14 +915,9 @@ void MyAvatar::simulate(float deltaTime, bool inView) { auto entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { - bool zoneAllowsFlying = true; - bool collisionlessAllowed = true; + std::pair zoneInteractionProperties; entityTree->withWriteLock([&] { - std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); - if (zone) { - zoneAllowsFlying = zone->getFlyingAllowed(); - collisionlessAllowed = zone->getGhostingAllowed(); - } + zoneInteractionProperties = entityTreeRenderer->getZoneInteractionProperties(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); forEachDescendant([&](SpatiallyNestablePointer object) { // we need to update attached queryAACubes in our own local tree so point-select always works @@ -935,6 +930,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) { }); }); bool isPhysicsEnabled = qApp->isPhysicsEnabled(); + bool zoneAllowsFlying = zoneInteractionProperties.first; + bool collisionlessAllowed = zoneInteractionProperties.second; _characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled); _characterController.setCollisionlessAllowed(collisionlessAllowed); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c71b296a74..d56e04b2ee 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1289,6 +1289,17 @@ CalculateEntityLoadingPriority EntityTreeRenderer::_calculateEntityLoadingPriori return 0.0f; }; +std::pair EntityTreeRenderer::getZoneInteractionProperties() { + for (auto& zone : _layeredZones) { + // Only domain entities control flying allowed and ghosting allowed + if (zone.zone && zone.zone->isDomainEntity()) { + return { zone.zone->getFlyingAllowed(), zone.zone->getGhostingAllowed() }; + } + } + + return { true, true }; +} + bool EntityTreeRenderer::wantsKeyboardFocus(const EntityItemID& id) const { auto renderable = renderableForEntityId(id); if (!renderable) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index d9f594a20b..725416e2cc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -105,7 +105,7 @@ public: // For Scene.shouldRenderEntities QList& getEntitiesLastInScene() { return _entityIDsLastInScene; } - std::shared_ptr myAvatarZone() { return _layeredZones.getZone(); } + std::pair getZoneInteractionProperties(); bool wantsKeyboardFocus(const EntityItemID& id) const; QObject* getEventHandler(const EntityItemID& id); diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 7cafaece7a..c1488a5893 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1414,8 +1414,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {Entities.Bloom} bloom - The bloom properties of the zone. * * @property {boolean} flyingAllowed=true - If true then visitors can fly in the zone; otherwise they cannot. + * Only works on domain entities. * @property {boolean} ghostingAllowed=true - If true then visitors with avatar collisions turned off will not - * collide with content in the zone; otherwise visitors will always collide with content in the zone. + * collide with content in the zone; otherwise visitors will always collide with content in the zone. Only works on domain entities. * @property {string} filterURL="" - The URL of a JavaScript file that filters changes to properties of entities within the * zone. It is periodically executed for each entity in the zone. It can, for example, be used to not allow changes to From 5438bddc84989d683ad0bf349c1e9fb2c4db1f08 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Thu, 31 Jan 2019 09:43:30 -0700 Subject: [PATCH 060/130] Fix assertion on shapeInfo when creating multisphere with empty data --- .../src/avatars-renderer/Avatar.cpp | 18 ++++++++++-------- libraries/shared/src/ShapeInfo.cpp | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 07c1ca9a32..ba5529e1c0 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1733,15 +1733,17 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { void Avatar::computeDetailedShapeInfo(ShapeInfo& shapeInfo, int jointIndex) { if (jointIndex > -1 && jointIndex < (int)_multiSphereShapes.size()) { auto& data = _multiSphereShapes[jointIndex].getSpheresData(); - std::vector positions; - std::vector radiuses; - positions.reserve(data.size()); - radiuses.reserve(data.size()); - for (auto& sphere : data) { - positions.push_back(sphere._position); - radiuses.push_back(sphere._radius); + if (data.size() > 0) { + std::vector positions; + std::vector radiuses; + positions.reserve(data.size()); + radiuses.reserve(data.size()); + for (auto& sphere : data) { + positions.push_back(sphere._position); + radiuses.push_back(sphere._radius); + } + shapeInfo.setMultiSphere(positions, radiuses); } - shapeInfo.setMultiSphere(positions, radiuses); } } diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 564d79bfda..c256cf2b76 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -152,7 +152,8 @@ void ShapeInfo::setSphere(float radius) { void ShapeInfo::setMultiSphere(const std::vector& centers, const std::vector& radiuses) { _url = ""; _type = SHAPE_TYPE_MULTISPHERE; - assert(centers.size() == radiuses.size() && centers.size() > 0); + assert(centers.size() == radiuses.size()); + assert(centers.size() > 0); for (size_t i = 0; i < centers.size(); i++) { SphereData sphere = SphereData(centers[i], radiuses[i]); _sphereCollection.push_back(sphere); From 15066faaf7a33147d7ea60f7de1cfc555fbb458c Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 31 Jan 2019 19:37:21 +0100 Subject: [PATCH 061/130] ignore pickRay for zone shape visualizers --- scripts/system/modules/entityShapeVisualizer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/modules/entityShapeVisualizer.js b/scripts/system/modules/entityShapeVisualizer.js index fe950c2e2b..fdf8ee81e7 100644 --- a/scripts/system/modules/entityShapeVisualizer.js +++ b/scripts/system/modules/entityShapeVisualizer.js @@ -135,6 +135,7 @@ EntityShape.prototype = { overlayProperties.canCastShadows = false; overlayProperties.parentID = this.entityID; overlayProperties.collisionless = true; + overlayProperties.ignorePickIntersection = true; this.entity = Entities.addEntity(overlayProperties, "local"); var PROJECTED_MATERIALS = false; this.materialEntity = Entities.addEntity({ From ee1552661c4ab5f093c6e6eaac0adf38d9651229 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 31 Jan 2019 12:04:46 -0800 Subject: [PATCH 062/130] fix textures --- .../src/RenderableModelEntityItem.cpp | 13 +++++------ .../src/RenderableModelEntityItem.h | 2 +- libraries/entities/src/ModelEntityItem.cpp | 11 +++++----- libraries/entities/src/ModelEntityItem.h | 1 - libraries/shared/src/RegisteredMetaTypes.cpp | 22 +++++++++---------- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7e01af04dd..2e53fa4d57 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1264,7 +1264,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin return false; } - if (_lastTextures != entity->getTextures()) { + if (_textures != entity->getTextures()) { return true; } @@ -1418,15 +1418,14 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce entity->_originalTexturesRead = true; } - if (_lastTextures != entity->getTextures()) { + if (_textures != entity->getTextures()) { + QVariantMap newTextures; withWriteLock([&] { _texturesLoaded = false; - _lastTextures = entity->getTextures(); + _textures = entity->getTextures(); + newTextures = parseTexturesToMap(_textures, entity->_originalTextures); }); - auto newTextures = parseTexturesToMap(_lastTextures, entity->_originalTextures); - if (newTextures != model->getTextures()) { - model->setTextures(newTextures); - } + model->setTextures(newTextures); } if (entity->_needsJointSimulation) { entity->copyAnimationJointDataToModel(); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 16c3664f28..2a37e7107f 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -181,7 +181,7 @@ private: bool _hasModel { false }; ModelPointer _model; - QString _lastTextures; + QString _textures; bool _texturesLoaded { false }; int _lastKnownCurrentFrame { -1 }; #ifdef MODEL_ENTITY_USE_FADE_EFFECT diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index ddbb028b6e..e365d0a7b6 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -43,14 +43,15 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem( } const QString ModelEntityItem::getTextures() const { - QReadLocker locker(&_texturesLock); - auto textures = _textures; - return textures; + return resultWithReadLock([&] { + return _textures; + }); } void ModelEntityItem::setTextures(const QString& textures) { - QWriteLocker locker(&_texturesLock); - _textures = textures; + withWriteLock([&] { + _textures = textures; + }); } EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 8c9fbdc45f..649a6cb50f 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -163,7 +163,6 @@ protected: AnimationPropertyGroup _animationProperties; - mutable QReadWriteLock _texturesLock; QString _textures; ShapeType _shapeType = SHAPE_TYPE_NONE; diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 5394a0f448..ec1126c92f 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1247,30 +1247,30 @@ void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector } } -QVariantMap parseTexturesToMap(QString textures, const QVariantMap& defaultTextures) { +QVariantMap parseTexturesToMap(QString newTextures, const QVariantMap& defaultTextures) { // If textures are unset, revert to original textures - if (textures.isEmpty()) { + if (newTextures.isEmpty()) { return defaultTextures; } // Legacy: a ,\n-delimited list of filename:"texturepath" - if (*textures.cbegin() != '{') { - textures = "{\"" + textures.replace(":\"", "\":\"").replace(",\n", ",\"") + "}"; + if (*newTextures.cbegin() != '{') { + newTextures = "{\"" + newTextures.replace(":\"", "\":\"").replace(",\n", ",\"") + "}"; } QJsonParseError error; - QJsonDocument texturesJson = QJsonDocument::fromJson(textures.toUtf8(), &error); + QJsonDocument newTexturesJson = QJsonDocument::fromJson(newTextures.toUtf8(), &error); // If textures are invalid, revert to original textures if (error.error != QJsonParseError::NoError) { - qWarning() << "Could not evaluate textures property value:" << textures; + qWarning() << "Could not evaluate textures property value:" << newTextures; return defaultTextures; } - QVariantMap texturesMap = texturesJson.toVariant().toMap(); - // If textures are unset, revert to original textures - if (texturesMap.isEmpty()) { - return defaultTextures; + QVariantMap newTexturesMap = newTexturesJson.toVariant().toMap(); + QVariantMap toReturn = defaultTextures; + for (auto& texture : newTexturesMap.keys()) { + toReturn[texture] = newTexturesMap[texture]; } - return texturesJson.toVariant().toMap(); + return toReturn; } \ No newline at end of file From 80f5cbc8ab0331ec057e8f2760d91bcd932b26e2 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 31 Jan 2019 22:12:35 +0100 Subject: [PATCH 063/130] also ignore picking on the material entity --- scripts/system/modules/entityShapeVisualizer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/modules/entityShapeVisualizer.js b/scripts/system/modules/entityShapeVisualizer.js index fdf8ee81e7..da28369cdd 100644 --- a/scripts/system/modules/entityShapeVisualizer.js +++ b/scripts/system/modules/entityShapeVisualizer.js @@ -147,6 +147,7 @@ EntityShape.prototype = { priority: 1, materialMappingMode: PROJECTED_MATERIALS ? "projected" : "uv", materialURL: Script.resolvePath("../assets/images/materials/GridPattern.json"), + ignorePickIntersection: true, }, "local"); }, update: function() { From ae09aec5d9c57ed42a214115e626f4bc02ac47b0 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 31 Jan 2019 13:36:53 -0800 Subject: [PATCH 064/130] embedded model geometry --- libraries/fbx/src/GLTFSerializer.cpp | 18 +++++++++++++++--- libraries/fbx/src/GLTFSerializer.h | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) mode change 100644 => 100755 libraries/fbx/src/GLTFSerializer.cpp mode change 100644 => 100755 libraries/fbx/src/GLTFSerializer.h diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp old mode 100644 new mode 100755 index 96c236f703..6db298ae58 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -939,10 +939,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas } bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { - QUrl binaryUrl = _url.resolved(url); - bool success; - std::tie(success, outdata) = requestData(binaryUrl); + + if (url.contains("data:application/octet-stream;base64,") || url.contains("data:image/png;base64,") || url.contains("data:image/jpeg;base64,")) { + QString binaryUrl = url.split(",")[1]; + std::tie(success, outdata) = requestEmbeddedData(binaryUrl); + } else { + QUrl binaryUrl = _url.resolved(url); + std::tie(success, outdata) = requestData(binaryUrl); + } return success; } @@ -975,6 +980,13 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { } } +std::tuple GLTFSerializer::requestEmbeddedData(QString binaryUrl) { + QByteArray urlBin = binaryUrl.toUtf8(); + QByteArray result = QByteArray::fromBase64(urlBin); + + return std::make_tuple(true, result); +} + QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) { if (!qApp) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h old mode 100644 new mode 100755 index 5fca77c4fd..fba03caa6b --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -772,6 +772,7 @@ private: QVector& out_vertices, QVector& out_normals); std::tuple requestData(QUrl& url); + std::tuple requestEmbeddedData(QString binaryUrl); QNetworkReply* request(QUrl& url, bool isTest); bool doesResourceExist(const QString& url); From 997660d430b6341a78a613d07916d76425a5ea6e Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 31 Jan 2019 13:50:17 -0800 Subject: [PATCH 065/130] review changes --- libraries/fbx/src/GLTFSerializer.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index c2fdc4f0bd..01e3cce560 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -843,14 +843,9 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { qWarning(modelformat) << "There was a problem reading glTF COLOR_0 data for model " << _url; continue; } - if (accessor.type == 3) { - for (int n = 0; n < colors.size(); n = n + 4) { - mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); - } - } else { - for (int n = 0; n < colors.size(); n = n + 3) { - mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); - } + int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; + for (int n = 0; n < colors.size() - 3; n += stride) { + mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); } } else if (key == "TEXCOORD_0") { QVector texcoords; From d0fb09a3bd2a792d8cef2717a4beff8b7808b3c4 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 31 Jan 2019 14:58:58 -0800 Subject: [PATCH 066/130] Assign lowest available suffix when display-names collide --- assignment-client/src/avatars/AvatarMixer.cpp | 62 +++++++++++++++---- assignment-client/src/avatars/AvatarMixer.h | 20 +++++- libraries/avatars/src/AvatarData.cpp | 9 ++- 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 500772c1b5..f885b8110d 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -38,6 +38,19 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; // FIXME - what we'd actually like to do is send to users at ~50% of their present rate down to 30hz. Assume 90 for now. const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; +const QRegularExpression AvatarMixer::suffixedNamePattern { R"(^\s*(.+)\s*_(\d)+\s*$)" }; + +// Lexicographic comparison: +bool AvatarMixer::SessionDisplayName::operator<(const SessionDisplayName& rhs) const { + if (_baseName < rhs._baseName) { + return true; + } else if (rhs._baseName < _baseName) { + return false; + } else { + return _suffix < rhs._suffix; + } +} + AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message), _slavePool(&_slaveSharedData) @@ -313,27 +326,40 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { bool sendIdentity = false; if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { AvatarData& avatar = nodeData->getAvatar(); - const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); - if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { - _sessionDisplayNames.remove(existingBaseDisplayName); + const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName(); + if (!existingBaseDisplayName.isEmpty()) { + SessionDisplayName existingDisplayName { existingBaseDisplayName }; + + auto suffixMatch = suffixedNamePattern.match(existingBaseDisplayName); + if (suffixMatch.hasMatch()) { + existingDisplayName._baseName = suffixMatch.captured(1); + existingDisplayName._suffix = suffixMatch.captured(2).toInt(); + } + _sessionDisplayNames.erase(existingDisplayName); } QString baseName = avatar.getDisplayName().trimmed(); const QRegularExpression curses { "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. - const QRegularExpression trailingDigits { "\\s*(_\\d+\\s*)?(\\s*\\n[^$]*)?$" }; // trailing whitespace "_123" and any subsequent lines + static const QRegularExpression trailingDigits { R"(\s*(_\d+\s*)?(\s*\n[^$]*)?$)" }; // trailing whitespace "_123" and any subsequent lines baseName = baseName.remove(trailingDigits); if (baseName.isEmpty()) { baseName = "anonymous"; } - QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. - int& highWater = soFar.first; - nodeData->setBaseDisplayName(baseName); - QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; + SessionDisplayName newDisplayName { baseName }; + auto nameIter = _sessionDisplayNames.lower_bound(newDisplayName); + if (nameIter != _sessionDisplayNames.end() && nameIter->_baseName == baseName) { + // Existing instance(s) of name; find first free suffix + while (*nameIter == newDisplayName && ++newDisplayName._suffix && ++nameIter != _sessionDisplayNames.end()) + ; + } + + _sessionDisplayNames.insert(newDisplayName); + QString sessionDisplayName = (newDisplayName._suffix > 0) ? baseName + "_" + QString::number(newDisplayName._suffix) : baseName; avatar.setSessionDisplayName(sessionDisplayName); - highWater++; - soFar.second++; // refcount + nodeData->setBaseDisplayName(baseName); + nodeData->flagIdentityChange(); nodeData->setAvatarSessionDisplayNameMustChange(false); sendIdentity = true; @@ -410,9 +436,19 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) { QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex()); AvatarMixerClientData* nodeData = dynamic_cast(avatarNode->getLinkedData()); const QString& baseDisplayName = nodeData->getBaseDisplayName(); - // No sense guarding against very rare case of a node with no entry, as this will work without the guard and do one less lookup in the common case. - if (--_sessionDisplayNames[baseDisplayName].second <= 0) { - _sessionDisplayNames.remove(baseDisplayName); + const QString& displayName = nodeData->getAvatar().getSessionDisplayName(); + SessionDisplayName exitingDisplayName { displayName }; + + auto suffixMatch = suffixedNamePattern.match(displayName); + if (suffixMatch.hasMatch()) { + exitingDisplayName._baseName = suffixMatch.captured(1); + exitingDisplayName._suffix = suffixMatch.captured(2).toInt(); + } + auto displayNameIter = _sessionDisplayNames.find(exitingDisplayName); + if (displayNameIter == _sessionDisplayNames.end()) { + qCDebug(avatars, "Exiting avatar displayname", displayName, "not found"); + } else { + _sessionDisplayNames.erase(displayNameIter); } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 764656a2d5..2992e19b8f 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -15,6 +15,7 @@ #ifndef hifi_AvatarMixer_h #define hifi_AvatarMixer_h +#include #include #include @@ -88,7 +89,24 @@ private: RateCounter<> _broadcastRate; p_high_resolution_clock::time_point _lastDebugMessage; - QHash> _sessionDisplayNames; + + // Pair of basename + uniquifying integer suffix. + struct SessionDisplayName { + explicit SessionDisplayName(QString baseName = QString(), int suffix = 0) : + _baseName(baseName), + _suffix(suffix) { } + // Does lexicographic ordering: + bool operator<(const SessionDisplayName& rhs) const; + bool operator==(const SessionDisplayName& rhs) const { + return _baseName == rhs._baseName && _suffix == rhs._suffix; + } + + QString _baseName; + int _suffix; + }; + static const QRegularExpression suffixedNamePattern; + + std::set _sessionDisplayNames; quint64 _displayNameManagementElapsedTime { 0 }; // total time spent in broadcastAvatarData/display name management... since last stats window quint64 _ignoreCalculationElapsedTime { 0 }; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 4e95774efb..c733cfa291 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -632,9 +632,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // include jointData if there is room for the most minimal section. i.e. no translations or rotations. IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) { - // Allow for faux joints + translation bit-vector: - const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) - + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE; + // Minimum space required for another rotation joint - + // size of joint + following translation bit-vector + translation scale + faux joints: + const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + + sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE; + auto startSection = destinationBuffer; // compute maxTranslationDimension before we send any joint data. @@ -724,6 +726,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const JointData& data = joints[i]; const JointData& last = lastSentJointData[i]; + // Note minSizeForJoint is conservative since there isn't a following bit-vector + scale. if (packetEnd - destinationBuffer >= minSizeForJoint) { if (!data.translationIsDefaultPose) { if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) From 3d1edf4d9eda443e49e6f1046df3dddb70c419af Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 31 Jan 2019 15:00:50 -0800 Subject: [PATCH 067/130] Make small code improvements to PrepareJointsTask --- .../model-baker/src/model-baker/PrepareJointsTask.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 20715cfed7..3b1a57cb43 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -62,10 +62,11 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu // Apply joint metadata from FST file mappings for (const auto& jointIn : jointsIn) { jointsOut.push_back(jointIn); - auto& jointOut = jointsOut[jointsOut.size()-1]; + auto& jointOut = jointsOut.back(); - if (jointNameMapping.contains(jointNameMapping.key(jointIn.name))) { - jointOut.name = jointNameMapping.key(jointIn.name); + auto jointNameMapKey = jointNameMapping.key(jointIn.name); + if (jointNameMapping.contains(jointNameMapKey)) { + jointOut.name = jointNameMapKey; } jointIndices.insert(jointOut.name, (int)jointsOut.size()); @@ -75,11 +76,11 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu auto offsets = getJointRotationOffsets(mapping); for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { QString jointName = itr.key(); - glm::quat rotationOffset = itr.value(); int jointIndex = jointIndices.value(jointName) - 1; if (jointIndex != -1) { + glm::quat rotationOffset = itr.value(); jointRotationOffsets.insert(jointIndex, rotationOffset); + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; } - qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; } } From e4ffc93bc254a4fb52f68ef4770efa730926daee Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 31 Jan 2019 15:19:05 -0800 Subject: [PATCH 068/130] Revert a cherry-picked workaround accidently committed This reverts commit 08b21109c1daac786a67490bfef658a359bf47bb. Stupid git - I never explicitly added this to the branch! --- libraries/avatars/src/AvatarData.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c733cfa291..4e95774efb 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -632,11 +632,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // include jointData if there is room for the most minimal section. i.e. no translations or rotations. IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) { - // Minimum space required for another rotation joint - - // size of joint + following translation bit-vector + translation scale + faux joints: - const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + - sizeof(float) + AvatarDataPacket::FAUX_JOINTS_SIZE; - + // Allow for faux joints + translation bit-vector: + const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE; auto startSection = destinationBuffer; // compute maxTranslationDimension before we send any joint data. @@ -726,7 +724,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent const JointData& data = joints[i]; const JointData& last = lastSentJointData[i]; - // Note minSizeForJoint is conservative since there isn't a following bit-vector + scale. if (packetEnd - destinationBuffer >= minSizeForJoint) { if (!data.translationIsDefaultPose) { if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) From 322030fccdf481c524c80da328761a350955d6f3 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 31 Jan 2019 16:07:26 -0800 Subject: [PATCH 069/130] Remove unused variable; fix use of qCDebug --- assignment-client/src/avatars/AvatarMixer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index f885b8110d..801f28c6f5 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -435,7 +435,6 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) { { // decrement sessionDisplayNames table and possibly remove QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex()); AvatarMixerClientData* nodeData = dynamic_cast(avatarNode->getLinkedData()); - const QString& baseDisplayName = nodeData->getBaseDisplayName(); const QString& displayName = nodeData->getAvatar().getSessionDisplayName(); SessionDisplayName exitingDisplayName { displayName }; @@ -446,7 +445,7 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) { } auto displayNameIter = _sessionDisplayNames.find(exitingDisplayName); if (displayNameIter == _sessionDisplayNames.end()) { - qCDebug(avatars, "Exiting avatar displayname", displayName, "not found"); + qCDebug(avatars) << "Exiting avatar displayname" << displayName << "not found"; } else { _sessionDisplayNames.erase(displayNameIter); } From f4118213b15fa12082a73990ed70e9af0678d0bf Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 1 Feb 2019 10:12:04 -0800 Subject: [PATCH 070/130] give tablet root color --- .../qml/hifi/commerce/common/sendAsset/SendAsset.qml | 3 +-- interface/resources/qml/hifi/tablet/TabletRoot.qml | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index bc8816e0ea..68d437a346 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -21,11 +21,10 @@ import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. -Rectangle { +Item { HifiConstants { id: hifi; } id: root; - color: hifi.colors.baseGray property int parentAppTitleBarHeight; property int parentAppNavBarHeight; property string currentActiveView: "sendAssetHome"; diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index b19dcbb919..93a23f1b9d 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -3,10 +3,13 @@ import Hifi 1.0 import "../../dialogs" import "../../controls" +import stylesUit 1.0 -Item { +Rectangle { + HifiConstants { id: hifi; } id: tabletRoot objectName: "tabletRoot" + color: hifi.colors.baseGray property string username: "Unknown user" property string usernameShort: "Unknown user" property var rootMenu; From d1d8832e7a9a9c32791c2f153738bf2dbc360f39 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Fri, 1 Feb 2019 11:27:43 -0800 Subject: [PATCH 071/130] read in embedded data --- libraries/fbx/src/GLTFSerializer.cpp | 29 ++++++++++++++++++++-------- libraries/fbx/src/GLTFSerializer.h | 3 ++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 6db298ae58..2ffcd2c728 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -352,9 +352,15 @@ bool GLTFSerializer::addImage(const QJsonObject& object) { QString mime; getStringVal(object, "uri", image.uri, image.defined); + if (image.uri.contains("data:image/png;base64,")) { + image.mimeType = getImageMimeType("image/png"); + } + if (image.uri.contains("data:image/jpeg;base64,")) { + image.mimeType = getImageMimeType("image/jpeg"); + } if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); - } + } getIntVal(object, "bufferView", image.bufferView, image.defined); _file.images.push_back(image); @@ -941,9 +947,8 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { bool success; - if (url.contains("data:application/octet-stream;base64,") || url.contains("data:image/png;base64,") || url.contains("data:image/jpeg;base64,")) { - QString binaryUrl = url.split(",")[1]; - std::tie(success, outdata) = requestEmbeddedData(binaryUrl); + if (url.contains("data:application/octet-stream;base64,")) { + std::tie(success, outdata) = std::make_tuple(true, requestEmbeddedData(url)); } else { QUrl binaryUrl = _url.resolved(url); std::tie(success, outdata) = requestData(binaryUrl); @@ -980,11 +985,13 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { } } -std::tuple GLTFSerializer::requestEmbeddedData(QString binaryUrl) { - QByteArray urlBin = binaryUrl.toUtf8(); - QByteArray result = QByteArray::fromBase64(urlBin); +QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { + QString binaryUrl = url.split(",")[1]; - return std::make_tuple(true, result); + QByteArray urlBin = binaryUrl.toUtf8(); + QByteArray data = QByteArray::fromBase64(urlBin); + + return data; } @@ -1018,11 +1025,17 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; + QString fname = QUrl(url).fileName(); QUrl textureUrl = _url.resolved(url); qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); + + if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,")) { + QByteArray result = requestEmbeddedData(url); + fbxtex.content = result; + } } return fbxtex; } diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index fba03caa6b..57ea126a7b 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -772,7 +772,8 @@ private: QVector& out_vertices, QVector& out_normals); std::tuple requestData(QUrl& url); - std::tuple requestEmbeddedData(QString binaryUrl); + QByteArray requestEmbeddedData(const QString& url); + QNetworkReply* request(QUrl& url, bool isTest); bool doesResourceExist(const QString& url); From 39ad36a4d03c2fdaad0216ddaeb2209c66cc3cf0 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 1 Feb 2019 15:27:17 -0800 Subject: [PATCH 072/130] QmlMarketplace - bugfixes and Markup rendering of descriptions * Render markup in descriptions (bold, italic, quote, etc.) * Back button from marketplaces list now works properly * Layout fixes --- .../hifi/commerce/marketplace/Marketplace.qml | 133 +++++++++++------ .../commerce/marketplace/MarketplaceItem.qml | 136 +++++++++++++----- scripts/system/html/js/marketplacesInject.js | 9 +- scripts/system/marketplaces/marketplaces.js | 13 +- 4 files changed, 210 insertions(+), 81 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index d32d298acd..a4cf260173 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -32,7 +32,7 @@ Rectangle { property string activeView: "initialize" property int currentSortIndex: 0 - property string sortString: "" + property string sortString: "recent" property string categoryString: "" property string searchString: "" property bool keyboardEnabled: HMD.active @@ -45,6 +45,7 @@ Rectangle { function getMarketplaceItems() { marketplaceItemView.visible = false; itemsList.visible = true; + licenseInfo.visible = false; marketBrowseModel.getFirstPage(); { if(root.searchString !== undefined && root.searchString !== "") { @@ -69,7 +70,6 @@ Rectangle { target: GlobalServices onMyUsernameChanged: { - console.log("LOGIN STATUS CHANGING"); Commerce.getLoginStatus(); } } @@ -333,7 +333,7 @@ Rectangle { break; } } - onTextChanged: root.searchString = text + onAccepted: { root.searchString = searchField.text; getMarketplaceItems(); @@ -474,11 +474,11 @@ Rectangle { anchors { fill: parent - topMargin: 120 + topMargin: 115 bottomMargin: 50 } - visible: true; + visible: true HifiModels.PSFListModel { id: marketBrowseModel @@ -565,17 +565,22 @@ Rectangle { header: Item { id: itemsHeading - + height: childrenRect.height width: parent.width - + + Rectangle { + id: itemsSpacer; + height: 20 + } + Rectangle { id: itemsLoginStatus; anchors { + top: itemsSpacer.bottom left: parent.left right: parent.right leftMargin: 15 - top: parent.top+15 } height: root.isLoggedIn ? 0 : 80 @@ -598,7 +603,7 @@ Rectangle { } width: 80; - text: root.price ? root.price : "LOG IN" + text: "LOG IN" onClicked: { sendToScript({method: 'needsLogIn_loginClicked'}); @@ -687,6 +692,7 @@ Rectangle { } Item { id: sort + visible: searchString === undefined || searchString === "" anchors { top: searchScope.bottom; @@ -695,7 +701,7 @@ Rectangle { topMargin: 10; leftMargin: 15; } - height: childrenRect.height + height: visible ? childrenRect.height : 0 RalewayRegular { id: sortText @@ -771,6 +777,7 @@ Rectangle { focus: true clip: true highlightFollowsCurrentItem: false + currentIndex: 1; delegate: SortButton { width: 80 @@ -818,25 +825,39 @@ Rectangle { id: marketplaceItemView anchors.fill: parent - anchors.topMargin: 120 + anchors.topMargin: 115 + anchors.bottomMargin: 50 width: parent.width visible: false - + ScrollView { id: marketplaceItemScrollView - anchors.fill: parent; + anchors.fill: parent clip: true ScrollBar.vertical.policy: ScrollBar.AlwaysOn contentWidth: parent.width + contentHeight: childrenRect.height + + function resize() { + contentHeight = (marketplaceItemContent.y - itemSpacer.y + marketplaceItemContent.height); + } + + Item { + id: itemSpacer + anchors.top: parent.top + height: 15 + } Rectangle { id: itemLoginStatus; anchors { left: parent.left right: parent.right + top: itemSpacer.bottom + topMargin: 10 leftMargin: 15 rightMargin: 15 } @@ -861,7 +882,7 @@ Rectangle { } width: 80; - text: root.price ? root.price : "LOG IN" + text: "LOG IN" onClicked: { sendToScript({method: 'needsLogIn_loginClicked'}); @@ -890,9 +911,9 @@ Rectangle { Rectangle { id: marketplaceItemContent - anchors.top: itemLoginStatus.bottom; + anchors.top: itemLoginStatus.bottom width: parent.width - height: childrenRect.height + 100 + height: childrenRect.height; RalewaySemiBold { id: backText @@ -900,6 +921,7 @@ Rectangle { anchors { top: parent.top left: parent.left + topMargin: 10 leftMargin: 15 bottomMargin: 10 } @@ -944,6 +966,10 @@ Rectangle { categoriesText.text = category; getMarketplaceItems(); } + + onResized: { + marketplaceItemScrollView.resize(); + } } } } @@ -975,37 +1001,64 @@ Rectangle { leftMargin: 15 } - HiFiGlyphs { - id: footerGlyph + Item { + id: footerText + anchors.fill: parent + visible: itemsList.visible + + HiFiGlyphs { + id: footerGlyph + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + rightMargin: 10 + } + + text: hifi.glyphs.info + size: 34 + color: hifi.colors.white + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: footerInfo + + anchors { + left: footerGlyph.right + top: parent.top + bottom: parent.bottom + } + + text: "Get items from Clara.io!" + color: hifi.colors.white + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + + HifiControlsUit.Button { anchors { left: parent.left top: parent.top bottom: parent.bottom + topMargin: 10 + bottomMargin: 10 + leftMargin: 10 rightMargin: 10 } - text: hifi.glyphs.info - size: 34 - color: hifi.colors.white - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } + visible: marketplaceItemView.visible + text: "< BACK" + width: 100 - RalewaySemiBold { - id: footerInfo - - anchors { - left: footerGlyph.right - top: parent.top - bottom: parent.bottom + onClicked: { + getMarketplaceItems(); } - - text: "Get items from Clara.io!" - color: hifi.colors.white - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - size: 18 } HifiControlsUit.Button { @@ -1023,7 +1076,7 @@ Rectangle { width: 180 onClicked: { - sendToScript({method: 'marketplace_marketplaces'}); + sendToScript({method: 'marketplace_marketplaces', itemId: marketplaceItemView.visible ? marketplaceItem.item_id : undefined}); } } } @@ -1041,7 +1094,7 @@ Rectangle { anchors { fill: root - topMargin: 100 + topMargin: 120 bottomMargin: 0 } @@ -1052,7 +1105,7 @@ Rectangle { anchors { bottomMargin: 1 - topMargin: 50 + topMargin: 60 leftMargin: 1 rightMargin: 1 fill: parent diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 0478f38764..b7e9a711d2 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -15,6 +15,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.9 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 +import QtWebEngine 1.5 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls @@ -49,6 +50,11 @@ Rectangle { categoriesListModel.append({"category":category}); }); } + + onDescriptionChanged: { + descriptionTextModel.clear(); + descriptionTextModel.append({text: description}) + } signal buy() signal categoryClicked(string category) @@ -63,7 +69,7 @@ Rectangle { onMarketplaceItemLikeResult: { if (result.status !== 'success') { - console.log("Failed to get Marketplace Categories", result.data.message); + console.log("Like/Unlike item", result.data.message); } else { root.liked = !root.liked; root.likes = root.liked ? root.likes + 1 : root.likes - 1; @@ -98,24 +104,33 @@ Rectangle { var sec = addLeadingZero(a.getSeconds()); return a.toDateString() + " " + drawnHour + ':' + min + amOrPm; } + function evalHeight() { + height = footer.y - header.y + footer.height; + } + + signal resized() + + onHeightChanged: { + resized(); + } anchors { - left: parent.left; - right: parent.right; - leftMargin: 15; - rightMargin: 15; + left: parent.left + right: parent.right + leftMargin: 15 + rightMargin: 15 } - height: childrenRect.height; + height: footer.y - header.y + footer.height Rectangle { id: header anchors { - left: parent.left; - right: parent.right; - top: parent.top; + left: parent.left + right: parent.right + top: parent.top } - height: 50; + height: 50 RalewaySemiBold { id: nameText @@ -137,10 +152,10 @@ Rectangle { id: likes anchors { - top: parent.top; - right: parent.right; - bottom: parent.bottom; - rightMargin: 5; + top: parent.top + right: parent.right + bottom: parent.bottom + rightMargin: 5 } RalewaySemiBold { @@ -216,7 +231,11 @@ Rectangle { right: parent.right; top: itemImage.bottom; } - height: childrenRect.height + height: categoriesList.y - buyButton.y + categoriesList.height + + function evalHeight() { + height = categoriesList.y - buyButton.y + categoriesList.height; + } HifiControlsUit.Button { id: buyButton @@ -309,7 +328,7 @@ Rectangle { top: postedLabel.bottom left: parent.left right: parent.right - topMargin: 10 + topMargin: 5 } text: { getFormattedDate(root.created_at); } @@ -360,6 +379,7 @@ Rectangle { anchors.top: licenseLabel.bottom anchors.left: parent.left + anchors.topMargin: 5 width: paintedWidth text: root.license @@ -371,9 +391,10 @@ Rectangle { RalewaySemiBold { id: licenseHelp - anchors.top: licenseText.bottom; - anchors.left: parent.left; - width: paintedWidth; + anchors.top: licenseText.bottom + anchors.left: parent.left + anchors.topMargin: 5 + width: paintedWidth text: "More about this license" size: 14 @@ -413,6 +434,7 @@ Rectangle { Item { id: descriptionItem + property string text: "" anchors { top: licenseItem.bottom @@ -421,13 +443,16 @@ Rectangle { right: parent.right } height: childrenRect.height - + onHeightChanged: { + footer.evalHeight(); + } RalewaySemiBold { id: descriptionLabel anchors.top: parent.top anchors.left: parent.left width: paintedWidth + height: 20 text: "DESCRIPTION:" size: 14 @@ -435,18 +460,60 @@ Rectangle { verticalAlignment: Text.AlignVCenter } - RalewaySemiBold { - id: descriptionText + //RalewaySemiBold { + // id: descriptionText + // + // anchors.top: descriptionLabel.bottom + // anchors.left: parent.left + // anchors.topMargin: 5 + // width: parent.width + // + // text: root.description + // size: 14 + // color: hifi.colors.lightGray + // verticalAlignment: Text.AlignVCenter + // wrapMode: Text.Wrap + //} + + + ListModel { + id: descriptionTextModel + } + + ListView { + id: descriptionTextView; anchors.top: descriptionLabel.bottom anchors.left: parent.left - width: parent.width + anchors.right: parent.right - text: root.description - size: 14 - color: hifi.colors.lightGray - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap + model: descriptionTextModel + interactive: false + + delegate: Component { + Rectangle { + id: descriptionWebRect + width: parent.width + height: 5 + WebEngineView { + id: descriptionWebView + anchors.fill: parent + + Component.onCompleted: { + loadHtml(""+model.text+""); + } + + onContentsSizeChanged: { + descriptionWebRect.height = contentsSize.height; + descriptionTextView.height = contentsSize.height; + } + + onNewViewRequested: function(request) { + sendToScript({method: 'marketplace_open_link', link: request.requestedUrl}); + } + } + } + } } } @@ -460,7 +527,7 @@ Rectangle { right: parent.right } width: parent.width - height: childrenRect.height + height: childrenRect.height + 50 RalewaySemiBold { id: categoryLabel @@ -480,12 +547,13 @@ Rectangle { ListView { anchors { - left: parent.left; - right: parent.right; - top: categoryLabel.bottom; + left: parent.left + right: parent.right + top: categoryLabel.bottom + bottomMargin: 15 } - height: 20*model.count + height: 24*model.count+10 model: categoriesListModel delegate: RalewaySemiBold { @@ -496,7 +564,7 @@ Rectangle { text: model.category size: 14 - height: 20 + height: 24 color: hifi.colors.blueHighlight verticalAlignment: Text.AlignVCenter diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 74bf8d3fec..8d408169ba 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -76,9 +76,12 @@ if (document.referrer !== "") { window.history.back(); } else { - EventBridge.emitWebEvent(JSON.stringify({ - type: GOTO_MARKETPLACE - })); + var params = { type: GOTO_MARKETPLACE }; + var itemIdMatch = location.search.match(/itemId=([^&]*)/); + if (itemIdMatch && itemIdMatch.length === 2) { + params.itemId = itemIdMatch[1]; + } + EventBridge.emitWebEvent(JSON.stringify(params)); } }); $("#all-markets").on("click", function () { diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index b7a6b951a9..c085763fad 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -41,7 +41,6 @@ var GOTO_DIRECTORY = "GOTO_DIRECTORY"; var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; -var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; var CLARA_DOWNLOAD_TITLE = "Preparing Download"; var messageBox = null; @@ -437,9 +436,8 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onWebEventReceived(message) { - message = JSON.parse(message); if (message.type === GOTO_MARKETPLACE) { - openMarketplace(); + openMarketplace(message.itemId); } else if (message.type === GOTO_DIRECTORY) { // This is the chooser between marketplaces. Only OUR markteplace // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. @@ -569,7 +567,14 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'marketplace_marketplaces': // This is the chooser between marketplaces. Only OUR markteplace // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. - ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + var url = MARKETPLACES_URL; + if(message.itemId) { + url = url + "?itemId=" + message.itemId + } + ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'marketplace_open_link': + ui.open(message.link); break; case 'checkout_rezClicked': case 'purchases_rezClicked': From 1d7265f668f0974daf21d07e671b9ed125485119 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 4 Feb 2019 09:45:06 -0800 Subject: [PATCH 073/130] trust the data in the packet, Luke --- libraries/octree/src/OctreePacketData.cpp | 27 ----------------------- 1 file changed, 27 deletions(-) diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index a79f0a0c2b..8ab502e951 100755 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -729,12 +729,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(glm::vec3) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(glm::vec3)); return sizeof(uint16_t) + length * sizeof(glm::vec3); @@ -744,14 +738,7 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(glm::quat) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); - const unsigned char *start = dataBytes; for (int i = 0; i < length; i++) { dataBytes += unpackOrientationQuatFromBytes(dataBytes, result[i]); @@ -764,12 +751,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(float) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(float)); return sizeof(uint16_t) + length * sizeof(float); @@ -779,14 +760,7 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length / 8 > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); - int bit = 0; unsigned char current = 0; const unsigned char *start = dataBytes; @@ -797,7 +771,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto result[i] = (bool)(current & (1 << bit)); bit = (bit + 1) % BITS_IN_BYTE; } - return (dataBytes - start) + (int)sizeof(uint16_t); } From 1840f874ab4e29e3c78a3ce9c649cac0e2724404 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 4 Feb 2019 13:29:06 -0800 Subject: [PATCH 074/130] Add Attributions to Qml Marketplace --- .../hifi/commerce/marketplace/Marketplace.qml | 6 +- .../commerce/marketplace/MarketplaceItem.qml | 118 ++++++++++++++++-- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index a4cf260173..8458e28ba8 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -369,7 +369,7 @@ Rectangle { categoriesButton.color = hifi.colors.white; } } - + Rectangle { anchors { left: parent.left; @@ -1057,7 +1057,9 @@ Rectangle { width: 100 onClicked: { - getMarketplaceItems(); + marketplaceItemView.visible = false; + itemsList.visible = true; + licenseInfo.visible = false; } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index b7e9a711d2..cb158a2b69 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -50,7 +50,15 @@ Rectangle { categoriesListModel.append({"category":category}); }); } - + + onAttributionsChanged: { + attributionsModel.clear(); + root.attributions.forEach(function(attribution) { + console.log("ATTRIBUITION:" + JSON.stringify(attribution)); + attributionsModel.append(attribution); + }); + } + onDescriptionChanged: { descriptionTextModel.clear(); descriptionTextModel.append({text: description}) @@ -339,24 +347,112 @@ Rectangle { } Rectangle { - anchors { - top: posted.bottom; - leftMargin: 15; - topMargin: 15; - } - width: parent.width; - height: 1; - color: hifi.colors.lightGray; + anchors { + top: posted.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: 1 + + color: hifi.colors.lightGrayText } + Item { + id: attributions + + anchors { + top: posted.bottom + topMargin: 30 + left: parent.left + right: parent.right + } + width: parent.width + height: attributionsModel.count > 0 ? childrenRect.height : 0 + visible: attributionsModel.count > 0 + + RalewaySemiBold { + id: attributionsLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "ATTRIBUTIONS:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + ListModel { + id: attributionsModel + } + + ListView { + anchors { + left: parent.left + right: parent.right + top: attributionsLabel.bottom + bottomMargin: 15 + } + + height: 24*model.count+10 + + model: attributionsModel + delegate: Item { + RalewaySemiBold { + id: attributionName + + anchors.leftMargin: 15 + width: paintedWidth + + text: model.name + size: 14 + height: 24 + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: attributionLink + + anchors.leftMargin: 15 + anchors.left: attributionName.right + width: paintedWidth + + text: "Link" + size: 14 + height: 24 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: attributionLink + + onClicked: sendToScript({method: 'marketplace_open_link', link: model.link}); + } + } + } + } + Rectangle { + + anchors { + bottom: attributions.bottom + leftMargin: 15 + } + width: parent.width + height: 1 + + color: hifi.colors.lightGrayText + } + } Item { id: licenseItem; anchors { - top: posted.bottom + top: attributions.bottom left: parent.left - topMargin: 30 + topMargin: 15 } width: parent.width height: childrenRect.height From e23c589de7f7db344256cc7471d8f29d8ee719ca Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 4 Feb 2019 15:04:18 -0800 Subject: [PATCH 075/130] QmlMarketplace - fix issue with footer not appearing in marketplaces list --- .../qml/hifi/commerce/marketplace/MarketplaceItem.qml | 6 ++++-- scripts/system/marketplaces/marketplaces.js | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index cb158a2b69..3d5b1c3bc8 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -262,7 +262,7 @@ Rectangle { color: hifi.buttons.blue onClicked: root.buy(); - } + } Item { id: creatorItem @@ -596,7 +596,9 @@ Rectangle { anchors.fill: parent Component.onCompleted: { - loadHtml(""+model.text+""); + descriptionWebView.enabled = false; + loadHtml(""+model.text+""); + descriptionWebView.enabled = true; } onContentsSizeChanged: { diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index c085763fad..e059081741 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -41,6 +41,7 @@ var GOTO_DIRECTORY = "GOTO_DIRECTORY"; var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; +var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; var CLARA_DOWNLOAD_TITLE = "Preparing Download"; var messageBox = null; @@ -436,6 +437,7 @@ function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onWebEventReceived(message) { + message = JSON.parse(message); if (message.type === GOTO_MARKETPLACE) { openMarketplace(message.itemId); } else if (message.type === GOTO_DIRECTORY) { From db9b44a71af5c1c9689af23d4b556a5ff53ce3dc Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 4 Feb 2019 15:30:53 -0800 Subject: [PATCH 076/130] QmlMarketplace - The QML Text object has sufficient markup to handle all of our needs, so use that instead of webengineview --- .../commerce/marketplace/MarketplaceItem.qml | 62 ++++--------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 3d5b1c3bc8..303a193d92 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -556,61 +556,21 @@ Rectangle { verticalAlignment: Text.AlignVCenter } - //RalewaySemiBold { - // id: descriptionText - // - // anchors.top: descriptionLabel.bottom - // anchors.left: parent.left - // anchors.topMargin: 5 - // width: parent.width - // - // text: root.description - // size: 14 - // color: hifi.colors.lightGray - // verticalAlignment: Text.AlignVCenter - // wrapMode: Text.Wrap - //} - - - ListModel { - id: descriptionTextModel - } - - ListView { - id: descriptionTextView; + RalewaySemiBold { + id: descriptionText anchors.top: descriptionLabel.bottom anchors.left: parent.left - anchors.right: parent.right - - model: descriptionTextModel - interactive: false + anchors.topMargin: 5 + width: parent.width - delegate: Component { - Rectangle { - id: descriptionWebRect - width: parent.width - height: 5 - WebEngineView { - id: descriptionWebView - anchors.fill: parent - - Component.onCompleted: { - descriptionWebView.enabled = false; - loadHtml(""+model.text+""); - descriptionWebView.enabled = true; - } - - onContentsSizeChanged: { - descriptionWebRect.height = contentsSize.height; - descriptionTextView.height = contentsSize.height; - } - - onNewViewRequested: function(request) { - sendToScript({method: 'marketplace_open_link', link: request.requestedUrl}); - } - } - } + text: root.description + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + onLinkActivated: { + sendToScript({method: 'marketplace_open_link', link: link}); } } } From 364a1698feadc52f5509fa605118b874dc3474d7 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 4 Feb 2019 17:24:30 -0800 Subject: [PATCH 077/130] QmlMarketplace - Height of Marketplace Item was improperly calcualted with attributions --- .../commerce/marketplace/MarketplaceItem.qml | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 303a193d92..bb757bedca 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -53,15 +53,13 @@ Rectangle { onAttributionsChanged: { attributionsModel.clear(); - root.attributions.forEach(function(attribution) { - console.log("ATTRIBUITION:" + JSON.stringify(attribution)); - attributionsModel.append(attribution); - }); - } - - onDescriptionChanged: { - descriptionTextModel.clear(); - descriptionTextModel.append({text: description}) + if(root.attributions) { + root.attributions.forEach(function(attribution) { + console.log("ATTRIBUITION:" + JSON.stringify(attribution)); + attributionsModel.append(attribution); + }); + } + footer.evalHeight(); } signal buy() @@ -243,6 +241,7 @@ Rectangle { function evalHeight() { height = categoriesList.y - buyButton.y + categoriesList.height; + console.log("HEIGHT: " + height); } HifiControlsUit.Button { @@ -378,6 +377,7 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left width: paintedWidth + height: paintedHeight text: "ATTRIBUTIONS:" size: 14 @@ -572,6 +572,8 @@ Rectangle { onLinkActivated: { sendToScript({method: 'marketplace_open_link', link: link}); } + + onHeightChanged: { footer.evalHeight(); } } } @@ -585,7 +587,9 @@ Rectangle { right: parent.right } width: parent.width - height: childrenRect.height + 50 + height: categoriesListModel.count*24 + categoryLabel.height + (isLoggedIn ? 50 : 150) + + onHeightChanged: { footer.evalHeight(); } RalewaySemiBold { id: categoryLabel From 5a4960b3001cdb32d88af6b0b4d5ca36a466ccf7 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 5 Feb 2019 08:57:18 -0800 Subject: [PATCH 078/130] add crash::doAssert() for debug purposes --- libraries/shared/src/CrashHelpers.cpp | 15 +++++++++++++++ libraries/shared/src/CrashHelpers.h | 1 + 2 files changed, 16 insertions(+) diff --git a/libraries/shared/src/CrashHelpers.cpp b/libraries/shared/src/CrashHelpers.cpp index f8ca90bc4c..1676318f3e 100644 --- a/libraries/shared/src/CrashHelpers.cpp +++ b/libraries/shared/src/CrashHelpers.cpp @@ -11,6 +11,16 @@ #include "CrashHelpers.h" +#ifdef NDEBUG +// undefine NDEBUG so doAssert() works for all builds +#undef NDEBUG +#include +#define NDEBUG +#else +#include +#endif + + namespace crash { class B; @@ -34,6 +44,11 @@ A::~A() { _b->virtualFunction(); } +// only use doAssert() for debug purposes +void doAssert(bool value) { + assert(value); +} + void pureVirtualCall() { qCDebug(shared) << "About to make a pure virtual call"; B b; diff --git a/libraries/shared/src/CrashHelpers.h b/libraries/shared/src/CrashHelpers.h index ad988c8906..247aea5cde 100644 --- a/libraries/shared/src/CrashHelpers.h +++ b/libraries/shared/src/CrashHelpers.h @@ -18,6 +18,7 @@ namespace crash { +void doAssert(bool value); // works for Release void pureVirtualCall(); void doubleFree(); void nullDeref(); From e50892b3d2b3b67292fabeda51443fe589a0b740 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 5 Feb 2019 08:57:55 -0800 Subject: [PATCH 079/130] MyAvatar is unmovable until physics is enabled --- interface/src/avatar/MyAvatar.cpp | 60 +++++++++++++------------------ interface/src/avatar/MyAvatar.h | 1 + 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 92d9270d20..a0deb721e2 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -847,6 +847,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { updateOrientation(deltaTime); updatePosition(deltaTime); + updateViewBoom(); } // update sensorToWorldMatrix for camera and hand controllers @@ -3323,21 +3324,22 @@ void MyAvatar::updateActionMotor(float deltaTime) { direction = Vectors::ZERO; } + float sensorToWorldScale = getSensorToWorldScale(); if (state == CharacterController::State::Hover) { // we're flying --> complex acceleration curve that builds on top of current motor speed and caps at some max speed float motorSpeed = glm::length(_actionMotorVelocity); - float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; + float finalMaxMotorSpeed = sensorToWorldScale * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; float speedGrowthTimescale = 2.0f; float speedIncreaseFactor = 1.8f * _walkSpeedScalar; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor; - const float maxBoostSpeed = getSensorToWorldScale() * MAX_BOOST_SPEED; + const float maxBoostSpeed = sensorToWorldScale * MAX_BOOST_SPEED; if (_isPushing) { if (motorSpeed < maxBoostSpeed) { // an active action motor should never be slower than this float boostCoefficient = (maxBoostSpeed - motorSpeed) / maxBoostSpeed; - motorSpeed += getSensorToWorldScale() * MIN_AVATAR_SPEED * boostCoefficient; + motorSpeed += sensorToWorldScale * MIN_AVATAR_SPEED * boostCoefficient; } else if (motorSpeed > finalMaxMotorSpeed) { motorSpeed = finalMaxMotorSpeed; } @@ -3348,45 +3350,21 @@ void MyAvatar::updateActionMotor(float deltaTime) { const glm::vec2 currentVel = { direction.x, direction.z }; float scaledSpeed = scaleSpeedByDirection(currentVel, _walkSpeed.get(), _walkBackwardSpeed.get()); // _walkSpeedScalar is a multiplier if we are in sprint mode, otherwise 1.0 - _actionMotorVelocity = getSensorToWorldScale() * (scaledSpeed * _walkSpeedScalar) * direction; - } - - float previousBoomLength = _boomLength; - float boomChange = getDriveKey(ZOOM); - _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; - _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); - - // May need to change view if boom length has changed - if (previousBoomLength != _boomLength) { - qApp->changeViewAsNeeded(_boomLength); + _actionMotorVelocity = sensorToWorldScale * (scaledSpeed * _walkSpeedScalar) * direction; } } void MyAvatar::updatePosition(float deltaTime) { - if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { - updateActionMotor(deltaTime); - } - - vec3 velocity = getWorldVelocity(); - float sensorToWorldScale = getSensorToWorldScale(); - float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; - const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s - if (!_characterController.isEnabledAndReady()) { - // _characterController is not in physics simulation but it can still compute its target velocity - updateMotors(); - _characterController.computeNewVelocity(deltaTime, velocity); - - float speed2 = glm::length(velocity); - if (speed2 > sensorToWorldScale2 * MIN_AVATAR_SPEED_SQUARED) { - // update position ourselves - applyPositionDelta(deltaTime * velocity); + if (_characterController.isEnabledAndReady()) { + if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { + updateActionMotor(deltaTime); } - measureMotionDerivatives(deltaTime); - _moving = speed2 > sensorToWorldScale2 * MOVING_SPEED_THRESHOLD_SQUARED; - } else { + float sensorToWorldScale = getSensorToWorldScale(); + float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; + vec3 velocity = getWorldVelocity(); float speed2 = glm::length2(velocity); + const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s _moving = speed2 > sensorToWorldScale2 * MOVING_SPEED_THRESHOLD_SQUARED; - if (_moving) { // scan for walkability glm::vec3 position = getWorldPosition(); @@ -3398,6 +3376,18 @@ void MyAvatar::updatePosition(float deltaTime) { } } +void MyAvatar::updateViewBoom() { + float previousBoomLength = _boomLength; + float boomChange = getDriveKey(ZOOM); + _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; + _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); + + // May need to change view if boom length has changed + if (previousBoomLength != _boomLength) { + qApp->changeViewAsNeeded(_boomLength); + } +} + void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { // COLLISION SOUND API in Audio has been removed } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0d27988543..c53eae65d4 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1732,6 +1732,7 @@ private: void updateOrientation(float deltaTime); void updateActionMotor(float deltaTime); void updatePosition(float deltaTime); + void updateViewBoom(); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void initHeadBones(); void initAnimGraph(); From d109c0fb1b6ab3d543d30442c6375b636b29f54c Mon Sep 17 00:00:00 2001 From: raveenajain Date: Tue, 5 Feb 2019 09:23:10 -0800 Subject: [PATCH 080/130] feedback changes --- libraries/fbx/src/GLTFSerializer.cpp | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 2ffcd2c728..197b3ee14c 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -354,13 +354,11 @@ bool GLTFSerializer::addImage(const QJsonObject& object) { getStringVal(object, "uri", image.uri, image.defined); if (image.uri.contains("data:image/png;base64,")) { image.mimeType = getImageMimeType("image/png"); - } - if (image.uri.contains("data:image/jpeg;base64,")) { + } else if (image.uri.contains("data:image/jpeg;base64,")) { image.mimeType = getImageMimeType("image/jpeg"); - } - if (getStringVal(object, "mimeType", mime, image.defined)) { + } else if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); - } + } getIntVal(object, "bufferView", image.bufferView, image.defined); _file.images.push_back(image); @@ -948,7 +946,8 @@ bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { bool success; if (url.contains("data:application/octet-stream;base64,")) { - std::tie(success, outdata) = std::make_tuple(true, requestEmbeddedData(url)); + outdata = requestEmbeddedData(url); + success = outdata.isEmpty() ? false : true; } else { QUrl binaryUrl = _url.resolved(url); std::tie(success, outdata) = requestData(binaryUrl); @@ -985,13 +984,9 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { } } -QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { - QString binaryUrl = url.split(",")[1]; - - QByteArray urlBin = binaryUrl.toUtf8(); - QByteArray data = QByteArray::fromBase64(urlBin); - - return data; +QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { + QString binaryUrl = url.split(",")[1]; + return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); } @@ -1033,8 +1028,7 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { fbxtex.filename = textureUrl.toEncoded(); if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,")) { - QByteArray result = requestEmbeddedData(url); - fbxtex.content = result; + fbxtex.content = requestEmbeddedData(url); } } return fbxtex; From 2b402609535e3b758df1ecd707ae9f162abd54d6 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 5 Feb 2019 09:27:12 -0800 Subject: [PATCH 081/130] QmlMarketplace - disable HTML for quest --- .../hifi/commerce/marketplace/Marketplace.qml | 43 +++++++++++---- .../commerce/marketplace/MarketplaceItem.qml | 54 +++++++++++-------- interface/src/Application.cpp | 7 +++ .../PlatformInfoScriptingInterface.cpp | 8 +++ .../PlatformInfoScriptingInterface.h | 6 +++ 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index a4cf260173..1db133c249 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -39,7 +39,8 @@ Rectangle { property bool keyboardRaised: false property string searchScopeString: "Featured" property bool isLoggedIn: false; - + property bool supports3DHTML: true; + anchors.fill: (typeof parent === undefined) ? undefined : parent function getMarketplaceItems() { @@ -60,6 +61,8 @@ Rectangle { Component.onCompleted: { Commerce.getLoginStatus(); + + supports3DHTML = PlatformInfo.has3DHTML(); } Component.onDestruction: { @@ -103,7 +106,7 @@ Rectangle { if (result.status !== 'success') { console.log("Failed to get Marketplace Item", result.data.message); } else { - + marketplaceItem.supports3DHTML = root.supports3DHTML; marketplaceItem.item_id = result.data.id; marketplaceItem.image_url = result.data.thumbnail_url; marketplaceItem.name = result.data.title; @@ -958,8 +961,16 @@ Rectangle { } onShowLicense: { - licenseInfoWebView.url = url; - licenseInfo.visible = true; + var xhr = new XMLHttpRequest; + xhr.open("GET", url); + xhr.onreadystatechange = function() { + if (xhr.readyState == XMLHttpRequest.DONE) { + console.log(xhr.responseText); + licenseText.text = xhr.responseText; + licenseInfo.visible = true; + } + }; + xhr.send(); } onCategoryClicked: { root.categoryString = category; @@ -1001,11 +1012,13 @@ Rectangle { leftMargin: 15 } + + Item { id: footerText anchors.fill: parent - visible: itemsList.visible + visible: root.supports3DHTML && itemsList.visible HiFiGlyphs { id: footerGlyph @@ -1072,6 +1085,8 @@ Rectangle { rightMargin: 10 } + visible: root.supports3DHTML + text: "SEE ALL MARKETS" width: 180 @@ -1100,16 +1115,24 @@ Rectangle { visible: false; - HifiControlsUit.WebView { - id: licenseInfoWebView - + ScrollView { anchors { bottomMargin: 1 topMargin: 60 - leftMargin: 1 - rightMargin: 1 + leftMargin: 15 fill: parent } + + RalewayRegular { + id: licenseText + + width:440 + wrapMode: Text.Wrap + + text: "" + size: 18; + color: hifi.colors.baseGray + } } Item { diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index b7e9a711d2..0c6d661609 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -43,6 +43,8 @@ Rectangle { property string created_at: "" property bool isLoggedIn: false; property int edition: -1; + property bool supports3DHTML: false; + onCategoriesChanged: { categoriesListModel.clear(); @@ -52,8 +54,13 @@ Rectangle { } onDescriptionChanged: { - descriptionTextModel.clear(); - descriptionTextModel.append({text: description}) + + if(root.supports3DHTML) { + descriptionTextModel.clear(); + descriptionTextModel.append({text: description}); + } else { + descriptionText.text = description; + } } signal buy() @@ -410,22 +417,22 @@ Rectangle { if (root.license === "No Rights Reserved (CC0)") { url = "https://creativecommons.org/publicdomain/zero/1.0/" } else if (root.license === "Attribution (CC BY)") { - url = "https://creativecommons.org/licenses/by/4.0/" + url = "https://creativecommons.org/licenses/by/4.0/legalcode.txt" } else if (root.license === "Attribution-ShareAlike (CC BY-SA)") { - url = "https://creativecommons.org/licenses/by-sa/4.0/" + url = "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt" } else if (root.license === "Attribution-NoDerivs (CC BY-ND)") { - url = "https://creativecommons.org/licenses/by-nd/4.0/" + url = "https://creativecommons.org/licenses/by-nd/4.0/legalcode.txt" } else if (root.license === "Attribution-NonCommercial (CC BY-NC)") { - url = "https://creativecommons.org/licenses/by-nc/4.0/" + url = "https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt" } else if (root.license === "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)") { - url = "https://creativecommons.org/licenses/by-nc-sa/4.0/" + url = "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt" } else if (root.license === "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { - url = "https://creativecommons.org/licenses/by-nc-nd/4.0/" + url = "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt" } else if (root.license === "Proof of Provenance License (PoP License)") { url = "https://digitalassetregistry.com/PoP-License/v1/" } if(url) { - licenseInfoWebView.url = url; + showLicense(url) } } } @@ -460,20 +467,21 @@ Rectangle { verticalAlignment: Text.AlignVCenter } - //RalewaySemiBold { - // id: descriptionText - // - // anchors.top: descriptionLabel.bottom - // anchors.left: parent.left - // anchors.topMargin: 5 - // width: parent.width - // - // text: root.description - // size: 14 - // color: hifi.colors.lightGray - // verticalAlignment: Text.AlignVCenter - // wrapMode: Text.Wrap - //} + RalewaySemiBold { + id: descriptionText + + anchors.top: descriptionLabel.bottom + anchors.left: parent.left + anchors.topMargin: 5 + width: parent.width + + visible: !root.supports3DHTML + + text: root.description + size: 14 + color: hifi.colors.lightGray + wrapMode: Text.Wrap + } ListModel { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 39ab3a8b1c..48c8f35ede 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3000,6 +3000,13 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, }, marketplaceCallback); + QmlContextCallback platformInfoCallback = [](QQmlContext* context) { + context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, platformInfoCallback); + QmlContextCallback ttsCallback = [](QQmlContext* context) { context->setContextProperty("TextToSpeech", DependencyManager::get().data()); }; diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index b6e4df0d40..b390ab7119 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -133,3 +133,11 @@ bool PlatformInfoScriptingInterface::hasRiftControllers() { bool PlatformInfoScriptingInterface::hasViveControllers() { return qApp->hasViveControllers(); } + +bool PlatformInfoScriptingInterface::has3DHTML() { +#if defined(Q_OS_ANDROID) + return false; +#else + return true; +#endif +} diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index 3ed57965c9..aece09b008 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -65,6 +65,12 @@ public slots: * @function Window.hasRift * @returns {boolean} true if running on Windows, otherwise false.*/ bool hasViveControllers(); + + /**jsdoc + * Returns true if device supports 3d HTML + * @function Window.hasRift + * @returns {boolean} true if device supports 3d HTML, otherwise false.*/ + bool has3DHTML(); }; #endif // hifi_PlatformInfoScriptingInterface_h From 21fa1878cb2904b12258cd95c6f7c7ff5d5cb9bb Mon Sep 17 00:00:00 2001 From: raveenajain Date: Tue, 5 Feb 2019 09:28:47 -0800 Subject: [PATCH 082/130] spaces --- libraries/fbx/src/GLTFSerializer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 197b3ee14c..674b4b1cf1 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -358,7 +358,7 @@ bool GLTFSerializer::addImage(const QJsonObject& object) { image.mimeType = getImageMimeType("image/jpeg"); } else if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); - } + } getIntVal(object, "bufferView", image.bufferView, image.defined); _file.images.push_back(image); @@ -984,7 +984,7 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { } } -QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { +QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { QString binaryUrl = url.split(",")[1]; return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); } From cbd83f972c312107bca27bb1d7ee6d7f07c5a60b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 5 Feb 2019 10:17:10 -0800 Subject: [PATCH 083/130] remove unsued variable --- interface/src/avatar/MyAvatar.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a0deb721e2..098d7943c7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -75,7 +75,6 @@ const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed const float MIN_AVATAR_SPEED = 0.05f; -const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f; float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; From 253e3554af8e5a483b44757a3a52e8ee4ac739f8 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Tue, 5 Feb 2019 10:18:34 -0800 Subject: [PATCH 084/130] feedback --- libraries/fbx/src/GLTFSerializer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 674b4b1cf1..e6b4652d6a 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -356,7 +356,8 @@ bool GLTFSerializer::addImage(const QJsonObject& object) { image.mimeType = getImageMimeType("image/png"); } else if (image.uri.contains("data:image/jpeg;base64,")) { image.mimeType = getImageMimeType("image/jpeg"); - } else if (getStringVal(object, "mimeType", mime, image.defined)) { + } + if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); } getIntVal(object, "bufferView", image.bufferView, image.defined); @@ -947,7 +948,7 @@ bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { if (url.contains("data:application/octet-stream;base64,")) { outdata = requestEmbeddedData(url); - success = outdata.isEmpty() ? false : true; + success = !outdata.isEmpty(); } else { QUrl binaryUrl = _url.resolved(url); std::tie(success, outdata) = requestData(binaryUrl); From cfaa841746ac97e3ef9f49ca0ccf087b98d4ae85 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 5 Feb 2019 12:34:42 -0800 Subject: [PATCH 085/130] Add output silence-detection to the noise gate processing --- libraries/audio/src/AudioGate.cpp | 108 +++++++++++++++++++++--------- libraries/audio/src/AudioGate.h | 9 ++- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index e9cdf832d2..0df46ac532 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -138,8 +138,8 @@ public: int32_t hysteresis(int32_t peak); int32_t envelope(int32_t attn); - virtual void process(int16_t* input, int16_t* output, int numFrames) = 0; - virtual void removeDC(int16_t* input, int16_t* output, int numFrames) = 0; + virtual bool process(int16_t* input, int16_t* output, int numFrames) = 0; + virtual bool removeDC(int16_t* input, int16_t* output, int numFrames) = 0; }; GateImpl::GateImpl(int sampleRate) { @@ -403,14 +403,15 @@ public: GateMono(int sampleRate) : GateImpl(sampleRate) {} // mono input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateMono::process(int16_t* input, int16_t* output, int numFrames) { +bool GateMono::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -453,15 +454,21 @@ void GateMono::process(int16_t* input, int16_t* output, int numFrames) { x = MULQ31(x, attn); // store 16-bit output - output[n] = (int16_t)saturateQ30(x); + x = saturateQ30(x); + output[n] = (int16_t)x; + + mask |= x; } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -471,8 +478,13 @@ void GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x); // store 16-bit output - output[n] = (int16_t)saturateQ30(x); + x = saturateQ30(x); + output[n] = (int16_t)x; + + mask |= x; } + + return mask != 0; } // @@ -489,14 +501,15 @@ public: GateStereo(int sampleRate) : GateImpl(sampleRate) {} // interleaved stereo input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateStereo::process(int16_t* input, int16_t* output, int numFrames) { +bool GateStereo::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -541,16 +554,23 @@ void GateStereo::process(int16_t* input, int16_t* output, int numFrames) { x1 = MULQ31(x1, attn); // store 16-bit output - output[2*n+0] = (int16_t)saturateQ30(x0); - output[2*n+1] = (int16_t)saturateQ30(x1); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + output[2*n+0] = (int16_t)x0; + output[2*n+1] = (int16_t)x1; + + mask |= (x0 | x1); } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -561,9 +581,15 @@ void GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x0, x1); // store 16-bit output - output[2*n+0] = (int16_t)saturateQ30(x0); - output[2*n+1] = (int16_t)saturateQ30(x1); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + output[2*n+0] = (int16_t)x0; + output[2*n+1] = (int16_t)x1; + + mask |= (x0 | x1); } + + return mask != 0; } // @@ -580,14 +606,15 @@ public: GateQuad(int sampleRate) : GateImpl(sampleRate) {} // interleaved quad input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateQuad::process(int16_t* input, int16_t* output, int numFrames) { +bool GateQuad::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -636,18 +663,27 @@ void GateQuad::process(int16_t* input, int16_t* output, int numFrames) { x3 = MULQ31(x3, attn); // store 16-bit output - output[4*n+0] = (int16_t)saturateQ30(x0); - output[4*n+1] = (int16_t)saturateQ30(x1); - output[4*n+2] = (int16_t)saturateQ30(x2); - output[4*n+3] = (int16_t)saturateQ30(x3); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + x2 = saturateQ30(x2); + x3 = saturateQ30(x3); + output[4*n+0] = (int16_t)x0; + output[4*n+1] = (int16_t)x1; + output[4*n+2] = (int16_t)x2; + output[4*n+3] = (int16_t)x3; + + mask |= (x0 | x1 | x2 | x3); } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -660,11 +696,19 @@ void GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x0, x1, x2, x3); // store 16-bit output - output[4*n+0] = (int16_t)saturateQ30(x0); - output[4*n+1] = (int16_t)saturateQ30(x1); - output[4*n+2] = (int16_t)saturateQ30(x2); - output[4*n+3] = (int16_t)saturateQ30(x3); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + x2 = saturateQ30(x2); + x3 = saturateQ30(x3); + output[4*n+0] = (int16_t)x0; + output[4*n+1] = (int16_t)x1; + output[4*n+2] = (int16_t)x2; + output[4*n+3] = (int16_t)x3; + + mask |= (x0 | x1 | x2 | x3); } + + return mask != 0; } // @@ -721,12 +765,12 @@ AudioGate::~AudioGate() { delete _impl; } -void AudioGate::render(int16_t* input, int16_t* output, int numFrames) { - _impl->process(input, output, numFrames); +bool AudioGate::render(int16_t* input, int16_t* output, int numFrames) { + return _impl->process(input, output, numFrames); } -void AudioGate::removeDC(int16_t* input, int16_t* output, int numFrames) { - _impl->removeDC(input, output, numFrames); +bool AudioGate::removeDC(int16_t* input, int16_t* output, int numFrames) { + return _impl->removeDC(input, output, numFrames); } void AudioGate::setThreshold(float threshold) { diff --git a/libraries/audio/src/AudioGate.h b/libraries/audio/src/AudioGate.h index d4ae3c5fe8..6fc7ca83df 100644 --- a/libraries/audio/src/AudioGate.h +++ b/libraries/audio/src/AudioGate.h @@ -18,9 +18,12 @@ public: AudioGate(int sampleRate, int numChannels); ~AudioGate(); - // interleaved int16_t input/output (in-place is allowed) - void render(int16_t* input, int16_t* output, int numFrames); - void removeDC(int16_t* input, int16_t* output, int numFrames); + // + // Process interleaved int16_t input/output (in-place is allowed). + // Returns true when output is non-zero. + // + bool render(int16_t* input, int16_t* output, int numFrames); + bool removeDC(int16_t* input, int16_t* output, int numFrames); void setThreshold(float threshold); void setRelease(float release); From 30bd9774b2d9127db8ddc1b857f689bab83d169a Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 5 Feb 2019 13:11:54 -0800 Subject: [PATCH 086/130] script engine to load platform specific files --- libraries/script-engine/src/ScriptEngines.cpp | 3 + libraries/shared/src/shared/FileUtils.cpp | 5 + .../+android_questInterface/defaultScripts.js | 118 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 scripts/+android_questInterface/defaultScripts.js diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 8ecfb84633..3963ad5593 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "ScriptEngine.h" #include "ScriptEngineLogging.h" @@ -476,6 +477,8 @@ ScriptEnginePointer ScriptEngines::loadScript(const QUrl& scriptFilename, bool i scriptUrl = normalizeScriptURL(scriptFilename); } + scriptUrl = QUrl(FileUtils::selectFile(scriptUrl.toString())); + auto scriptEngine = getScriptEngine(scriptUrl); if (scriptEngine && !scriptEngine->isStopping()) { return scriptEngine; diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 0709a53602..041fca5459 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -30,6 +30,11 @@ const QStringList& FileUtils::getFileSelectors() { static std::once_flag once; static QStringList extraSelectors; std::call_once(once, [] { + +#if defined(Q_OS_ANDROID) + //extraSelectors << "android_" HIFI_ANDROID_APP; +#endif + #if defined(USE_GLES) extraSelectors << "gles"; #endif diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js new file mode 100644 index 0000000000..da50e4a182 --- /dev/null +++ b/scripts/+android_questInterface/defaultScripts.js @@ -0,0 +1,118 @@ +"use strict"; +/* jslint vars: true, plusplus: true */ + +// +// defaultScripts.js +// examples +// +// 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 +// + +var DEFAULT_SCRIPTS_COMBINED = [ + "system/request-service.js", + "system/progress.js", + //"system/away.js", + "system/hmd.js", + "system/menu.js", + "system/bubble.js", + "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/avatarapp.js", + "system/makeUserConnection.js", + "system/tablet-goto.js", + "system/notifications.js", + "system/commerce/wallet.js", + "system/dialTone.js", + "system/firstPersonHMD.js", + "system/tablet-ui/tabletUI.js", + "system/miniTablet.js" +]; +var DEFAULT_SCRIPTS_SEPARATE = [ + "system/controllers/controllerScripts.js", + //"system/chat.js" +]; + +if (Window.interstitialModeEnabled) { + // Insert interstitial scripts at front so that they're started first. + DEFAULT_SCRIPTS_COMBINED.splice(0, 0, "system/interstitialPage.js", "system/redirectOverlays.js"); +} + +// add a menu item for debugging +var MENU_CATEGORY = "Developer > Scripting"; +var MENU_ITEM = "Debug defaultScripts.js"; + +var SETTINGS_KEY = '_debugDefaultScriptsIsChecked'; +var previousSetting = Settings.getValue(SETTINGS_KEY); + +if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { + previousSetting = false; +} + +if (previousSetting === true || previousSetting === 'true') { + previousSetting = true; +} + +if (Menu.menuExists(MENU_CATEGORY) && !Menu.menuItemExists(MENU_CATEGORY, MENU_ITEM)) { + Menu.addMenuItem({ + menuName: MENU_CATEGORY, + menuItemName: MENU_ITEM, + isCheckable: true, + isChecked: previousSetting, + }); +} + +function loadSeparateDefaults() { + for (var i in DEFAULT_SCRIPTS_SEPARATE) { + Script.load(DEFAULT_SCRIPTS_SEPARATE[i]); + } +} + +function runDefaultsTogether() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.include(DEFAULT_SCRIPTS_COMBINED[i]); + } + loadSeparateDefaults(); +} + +function runDefaultsSeparately() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.load(DEFAULT_SCRIPTS_COMBINED[i]); + } + loadSeparateDefaults(); +} + +// start all scripts +if (Menu.isOptionChecked(MENU_ITEM)) { + // we're debugging individual default scripts + // so we load each into its own ScriptEngine instance + runDefaultsSeparately(); +} else { + // include all default scripts into this ScriptEngine + runDefaultsTogether(); +} + +function menuItemEvent(menuItem) { + if (menuItem === MENU_ITEM) { + var isChecked = Menu.isOptionChecked(MENU_ITEM); + if (isChecked === true) { + Settings.setValue(SETTINGS_KEY, true); + } else if (isChecked === false) { + Settings.setValue(SETTINGS_KEY, false); + } + Menu.triggerOption("Reload All Scripts"); + } +} + +function removeMenuItem() { + if (!Menu.isOptionChecked(MENU_ITEM)) { + Menu.removeMenuItem(MENU_CATEGORY, MENU_ITEM); + } +} + +Script.scriptEnding.connect(function() { + removeMenuItem(); +}); + +Menu.menuItemEvent.connect(menuItemEvent); From 569bef50fdaea8b51abf14c8ed9afc935c84ed16 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 14:26:57 -0800 Subject: [PATCH 087/130] AnimPose operator* optimizations --- interface/src/avatar/AvatarActionHold.cpp | 2 +- interface/src/avatar/MyAvatar.cpp | 6 +- interface/src/avatar/MySkeletonModel.cpp | 6 +- libraries/animation/src/AnimClip.cpp | 8 +- .../animation/src/AnimInverseKinematics.cpp | 14 +-- libraries/animation/src/AnimManipulator.cpp | 2 +- .../src/AnimPoleVectorConstraint.cpp | 2 +- libraries/animation/src/AnimPose.cpp | 36 ++---- libraries/animation/src/AnimPose.h | 11 +- libraries/animation/src/Rig.cpp | 9 +- .../src/avatars-renderer/Avatar.cpp | 4 +- libraries/render-utils/src/AnimDebugDraw.cpp | 8 +- .../render-utils/src/CauterizedModel.cpp | 6 +- libraries/render-utils/src/Model.cpp | 2 +- libraries/render-utils/src/Model.h | 6 +- tests/animation/src/AnimTests.cpp | 105 ++++++++++++++++-- tests/animation/src/AnimTests.h | 1 + 17 files changed, 151 insertions(+), 77 deletions(-) diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 5fb9c9a0ee..a1826076fa 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -569,7 +569,7 @@ void AvatarActionHold::lateAvatarUpdate(const AnimPose& prePhysicsRoomPose, cons } btTransform worldTrans = rigidBody->getWorldTransform(); - AnimPose worldBodyPose(glm::vec3(1), bulletToGLM(worldTrans.getRotation()), bulletToGLM(worldTrans.getOrigin())); + AnimPose worldBodyPose(1.0f, bulletToGLM(worldTrans.getRotation()), bulletToGLM(worldTrans.getOrigin())); // transform the body transform into sensor space with the prePhysics sensor-to-world matrix. // then transform it back into world uisng the postAvatarUpdate sensor-to-world matrix. diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 92d9270d20..1d3fb5a5aa 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2983,7 +2983,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { auto animSkeleton = _skeletonModel->getRig().getAnimSkeleton(); // the rig is in the skeletonModel frame - AnimPose xform(glm::vec3(1), _skeletonModel->getRotation(), _skeletonModel->getTranslation()); + AnimPose xform(1.0f, _skeletonModel->getRotation(), _skeletonModel->getTranslation()); if (_enableDebugDrawDefaultPose && animSkeleton) { glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); @@ -3028,7 +3028,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { updateHoldActions(_prePhysicsRoomPose, postUpdateRoomPose); if (_enableDebugDrawDetailedCollision) { - AnimPose rigToWorldPose(glm::vec3(1.0f), getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); + AnimPose rigToWorldPose(1.0f, getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); const int NUM_DEBUG_COLORS = 8; const glm::vec4 DEBUG_COLORS[NUM_DEBUG_COLORS] = { glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), @@ -4835,7 +4835,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); // remove scale present from sensorToWorldMatrix - followWorldPose.scale() = glm::vec3(1.0f); + followWorldPose.scale() = 1.0f; if (isActive(Rotation)) { //use the hmd reading for the hips follow diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 26d69841d0..253cc891ee 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -41,7 +41,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { if (myAvatar->isJointPinned(hipsIndex)) { Transform avatarTransform = myAvatar->getTransform(); AnimPose result = AnimPose(worldToSensorMat * avatarTransform.getMatrix() * Matrices::Y_180); - result.scale() = glm::vec3(1.0f, 1.0f, 1.0f); + result.scale() = 1.0f; return result; } @@ -108,7 +108,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Rig::ControllerParameters params; - AnimPose avatarToRigPose(glm::vec3(1.0f), Quaternions::Y_180, glm::vec3(0.0f)); + AnimPose avatarToRigPose(1.0f, Quaternions::Y_180, glm::vec3(0.0f)); glm::mat4 rigToAvatarMatrix = Matrices::Y_180; glm::mat4 avatarToWorldMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); @@ -127,7 +127,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // preMult 180 is necessary to convert from avatar to rig coordinates. // postMult 180 is necessary to convert head from -z forward to z forward. glm::quat headRot = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame() * Quaternions::Y_180; - params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = AnimPose(glm::vec3(1.0f), headRot, glm::vec3(0.0f)); + params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = AnimPose(1.0f, headRot, glm::vec3(0.0f)); params.primaryControllerFlags[Rig::PrimaryControllerType_Head] = 0; } diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 1adc04ee1b..71b876ff8c 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -140,10 +140,10 @@ void AnimClip::copyFromNetworkAnim() { postRot = animSkeleton.getPostRotationPose(animJoint); // cancel out scale - preRot.scale() = glm::vec3(1.0f); - postRot.scale() = glm::vec3(1.0f); + preRot.scale() = 1.0f; + postRot.scale() = 1.0f; - AnimPose rot(glm::vec3(1.0f), hfmAnimRot, glm::vec3()); + AnimPose rot(1.0f, hfmAnimRot, glm::vec3()); // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. @@ -155,7 +155,7 @@ void AnimClip::copyFromNetworkAnim() { boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(hfmZeroTrans); } - AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); + AnimPose trans = AnimPose(1.0f, glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); _anim[frame][skeletonJoint] = trans * preRot * rot * postRot; } diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index a1809f3438..7af9e81889 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -552,7 +552,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const AnimPose accum = absolutePoses[_hipsIndex]; AnimPose baseParentPose = absolutePoses[_hipsIndex]; for (int i = (int)chainDepth - 1; i >= 0; i--) { - accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); + accum = accum * AnimPose(1.0f, jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); postAbsPoses[i] = accum; if (jointChainInfoOut.jointInfoVec[i].jointIndex == topJointIndex) { topChainIndex = i; @@ -734,7 +734,7 @@ void AnimInverseKinematics::computeAndCacheSplineJointInfosForIKTarget(const Ani glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); - AnimPose pose(glm::vec3(1.0f), rot, spline(t)); + AnimPose pose(1.0f, rot, spline(t)); AnimPose offsetPose = pose.inverse() * defaultPose; SplineJointInfo splineJointInfo = { index, ratio, offsetPose }; @@ -767,7 +767,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co const int baseIndex = _hipsIndex; // build spline from tip to base - AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose tipPose = AnimPose(1.0f, target.getRotation(), target.getTranslation()); AnimPose basePose = absolutePoses[baseIndex]; CubicHermiteSplineFunctorWithArcLength spline; if (target.getIndex() == _headIndex) { @@ -815,7 +815,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); - AnimPose desiredAbsPose = AnimPose(glm::vec3(1.0f), rot, trans) * splineJointInfo.offsetPose; + AnimPose desiredAbsPose = AnimPose(1.0f, rot, trans) * splineJointInfo.offsetPose; // apply flex coefficent AnimPose flexedAbsPose; @@ -965,7 +965,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } _relativePoses[_hipsIndex] = parentAbsPose.inverse() * absPose; - _relativePoses[_hipsIndex].scale() = glm::vec3(1.0f); + _relativePoses[_hipsIndex].scale() = 1.0f; } // if there is an active jointChainInfo for the hips store the post shifted hips into it. @@ -1753,7 +1753,7 @@ void AnimInverseKinematics::setSecondaryTargets(const AnimContext& context) { AnimPose rigToGeometryPose = AnimPose(glm::inverse(context.getGeometryToRigMatrix())); for (auto& iter : _secondaryTargetsInRigFrame) { AnimPose absPose = rigToGeometryPose * iter.second; - absPose.scale() = glm::vec3(1.0f); + absPose.scale() = 1.0f; AnimPose parentAbsPose; int parentIndex = _skeleton->getParentIndex(iter.first); @@ -1825,7 +1825,7 @@ void AnimInverseKinematics::debugDrawSpineSplines(const AnimContext& context, co const int baseIndex = _hipsIndex; // build spline - AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose tipPose = AnimPose(1.0f, target.getRotation(), target.getTranslation()); AnimPose basePose = _skeleton->getAbsolutePose(baseIndex, _relativePoses); CubicHermiteSplineFunctorWithArcLength spline; diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index 1146cbb19a..c75c9865bb 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -172,5 +172,5 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap& break; } - return AnimPose(glm::vec3(1), relRot, relTrans); + return AnimPose(1.0f, relRot, relTrans); } diff --git a/libraries/animation/src/AnimPoleVectorConstraint.cpp b/libraries/animation/src/AnimPoleVectorConstraint.cpp index f017fe2348..96e4e67261 100644 --- a/libraries/animation/src/AnimPoleVectorConstraint.cpp +++ b/libraries/animation/src/AnimPoleVectorConstraint.cpp @@ -95,7 +95,7 @@ const AnimPoseVec& AnimPoleVectorConstraint::evaluate(const AnimVariantMap& anim AnimPose tipPose = ikChain.getAbsolutePoseFromJointIndex(_tipJointIndex); // Look up refVector from animVars, make sure to convert into geom space. - glm::vec3 refVector = midPose.xformVectorFast(_referenceVector); + glm::vec3 refVector = midPose.xformVector(_referenceVector); float refVectorLength = glm::length(refVector); glm::vec3 axis = basePose.trans() - tipPose.trans(); diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index d77514e691..7b80e96ed5 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -14,15 +14,14 @@ #include #include "AnimUtil.h" -const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), - glm::quat(), - glm::vec3(0.0f)); +const AnimPose AnimPose::identity = AnimPose(1.0f, glm::quat(), glm::vec3(0.0f)); AnimPose::AnimPose(const glm::mat4& mat) { static const float EPSILON = 0.0001f; - _scale = extractScale(mat); + glm::vec3 scale = extractScale(mat); // quat_cast doesn't work so well with scaled matrices, so cancel it out. - glm::mat4 tmp = glm::scale(mat, 1.0f / _scale); + glm::mat4 tmp = glm::scale(mat, 1.0f / scale); + _scale = extractUniformScale(scale); _rot = glm::quat_cast(tmp); float lengthSquared = glm::length2(_rot); if (glm::abs(lengthSquared - 1.0f) > EPSILON) { @@ -40,25 +39,15 @@ glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { return *this * rhs; } -// really slow, but accurate for transforms with non-uniform scale glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { - glm::vec3 xAxis = _rot * glm::vec3(_scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale.y, 0.0f); - glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale.z); - glm::mat3 mat(xAxis, yAxis, zAxis); - glm::mat3 transInvMat = glm::inverse(glm::transpose(mat)); - return transInvMat * rhs; -} - -// faster, but does not handle non-uniform scale correctly. -glm::vec3 AnimPose::xformVectorFast(const glm::vec3& rhs) const { return _rot * (_scale * rhs); } AnimPose AnimPose::operator*(const AnimPose& rhs) const { - glm::mat4 result; - glm_mat4u_mul(*this, rhs, result); - return AnimPose(result); + float scale = _scale * rhs._scale; + glm::quat rot = _rot * rhs._rot; + glm::vec3 trans = _trans + (_rot * (_scale * rhs._trans)); + return AnimPose(scale, rot, trans); } AnimPose AnimPose::inverse() const { @@ -71,11 +60,10 @@ AnimPose AnimPose::mirror() const { } AnimPose::operator glm::mat4() const { - glm::vec3 xAxis = _rot * glm::vec3(_scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale.y, 0.0f); - glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale.z); - return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), - glm::vec4(zAxis, 0.0f), glm::vec4(_trans, 1.0f)); + glm::vec3 xAxis = _rot * glm::vec3(_scale, 0.0f, 0.0f); + glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale, 0.0f); + glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale); + return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), glm::vec4(zAxis, 0.0f), glm::vec4(_trans, 1.0f)); } void AnimPose::blend(const AnimPose& srcPose, float alpha) { diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h index 1558a6b881..89dcbaf2ab 100644 --- a/libraries/animation/src/AnimPose.h +++ b/libraries/animation/src/AnimPose.h @@ -23,12 +23,11 @@ public: explicit AnimPose(const glm::mat4& mat); explicit AnimPose(const glm::quat& rotIn) : _scale(1.0f), _rot(rotIn), _trans(0.0f) {} AnimPose(const glm::quat& rotIn, const glm::vec3& transIn) : _scale(1.0f), _rot(rotIn), _trans(transIn) {} - AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _scale(scaleIn), _rot(rotIn), _trans(transIn) {} + AnimPose(float scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _scale(scaleIn), _rot(rotIn), _trans(transIn) {} static const AnimPose identity; glm::vec3 xformPoint(const glm::vec3& rhs) const; glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow, but accurate for transforms with non-uniform scale - glm::vec3 xformVectorFast(const glm::vec3& rhs) const; // faster, but does not handle non-uniform scale correctly. glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint AnimPose operator*(const AnimPose& rhs) const; @@ -37,8 +36,8 @@ public: AnimPose mirror() const; operator glm::mat4() const; - const glm::vec3& scale() const { return _scale; } - glm::vec3& scale() { return _scale; } + const float scale() const { return _scale; } + float& scale() { return _scale; } const glm::quat& rot() const { return _rot; } glm::quat& rot() { return _rot; } @@ -50,13 +49,13 @@ public: private: friend QDebug operator<<(QDebug debug, const AnimPose& pose); - glm::vec3 _scale { 1.0f }; glm::quat _rot; glm::vec3 _trans; + float _scale { 1.0f }; // uniform scale only. }; inline QDebug operator<<(QDebug debug, const AnimPose& pose) { - debug << "AnimPose, trans = (" << pose.trans().x << pose.trans().y << pose.trans().z << "), rot = (" << pose.rot().x << pose.rot().y << pose.rot().z << pose.rot().w << "), scale = (" << pose.scale().x << pose.scale().y << pose.scale().z << ")"; + debug << "AnimPose, trans = (" << pose.trans().x << pose.trans().y << pose.trans().z << "), rot = (" << pose.rot().x << pose.rot().y << pose.rot().z << pose.rot().w << "), scale =" << pose.scale(); return debug; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 7842ec0804..2fdbc8addb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -54,6 +54,11 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) { return 1.0f - fabsf(glm::dot(p, q)) <= EPSILON; } +static bool isEqual(const float p, float q) { + const float EPSILON = 0.00001f; + return fabsf(p - q) <= EPSILON; +} + #define ASSERT(cond) assert(cond) // 2 meter tall dude @@ -1329,7 +1334,7 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) { // we are within the k-dop so push the point along the minimum displacement found - displacementOut = shapePose.xformVectorFast(minDisplacement); + displacementOut = shapePose.xformVector(minDisplacement); return true; } else { // point is outside of kdop @@ -1338,7 +1343,7 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } else { // point is directly on top of shapeInfo.avgPoint. // push the point out along the x axis. - displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]); + displacementOut = shapePose.xformVector(shapeInfo.points[0]); return true; } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 07c1ca9a32..c195206dcb 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1972,12 +1972,12 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const { auto& rig = _skeletonModel->getRig(); // Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here. - AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans()); + AnimPose modelOffsetWithoutAvatarScale(1.0f, rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans()); AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * rig.getGeometryOffsetPose(); // This factor can be used to scale distances in the geometry frame into the unscaled rig frame. // Typically it will be the unit conversion from cm to m. - float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor. + float scaleFactor = geomToRigWithoutAvatarScale.scale(); int headTopJoint = rig.indexOfJoint("HeadTop_End"); int headJoint = rig.indexOfJoint("Head"); diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index bf528ee5f0..8944ae7996 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -374,7 +374,7 @@ void AnimDebugDraw::update() { glm::vec4 color = std::get<3>(iter.second); for (int i = 0; i < skeleton->getNumJoints(); i++) { - const float radius = BONE_RADIUS / (absPoses[i].scale().x * rootPose.scale().x); + const float radius = BONE_RADIUS / (absPoses[i].scale() * rootPose.scale()); // draw bone addBone(rootPose, absPoses[i], radius, color, v); @@ -394,16 +394,16 @@ void AnimDebugDraw::update() { glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); const float radius = POSE_RADIUS; - addBone(AnimPose::identity, AnimPose(glm::vec3(1), rot, pos), radius, color, v); + addBone(AnimPose::identity, AnimPose(1.0f, rot, pos), radius, color, v); } - AnimPose myAvatarPose(glm::vec3(1), DebugDraw::getInstance().getMyAvatarRot(), DebugDraw::getInstance().getMyAvatarPos()); + AnimPose myAvatarPose(1.0f, DebugDraw::getInstance().getMyAvatarRot(), DebugDraw::getInstance().getMyAvatarPos()); for (auto& iter : myAvatarMarkerMap) { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); const float radius = POSE_RADIUS; - addBone(myAvatarPose, AnimPose(glm::vec3(1), rot, pos), radius, color, v); + addBone(myAvatarPose, AnimPose(1.0f, rot, pos), radius, color, v); } // draw rays from shared DebugDraw singleton diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 81a81c5602..b70925201a 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -122,7 +122,7 @@ void CauterizedModel::updateClusterMatrices() { if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform jointTransform(jointPose.rot(), glm::vec3(jointPose.scale()), jointPose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); @@ -138,7 +138,7 @@ void CauterizedModel::updateClusterMatrices() { if (!_cauterizeBoneSet.empty()) { AnimPose cauterizePose = _rig.getJointPose(_rig.indexOfJoint("Neck")); - cauterizePose.scale() = glm::vec3(0.0001f, 0.0001f, 0.0001f); + cauterizePose.scale() = 0.0001f; static const glm::mat4 zeroScale( glm::vec4(0.0001f, 0.0f, 0.0f, 0.0f), @@ -161,7 +161,7 @@ void CauterizedModel::updateClusterMatrices() { // not cauterized so just copy the value from the non-cauterized version. state.clusterDualQuaternions[j] = _meshStates[i].clusterDualQuaternions[j]; } else { - Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); + Transform jointTransform(cauterizePose.rot(), glm::vec3(cauterizePose.scale()), cauterizePose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index da8dceb176..c68c9259e5 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1368,7 +1368,7 @@ void Model::updateClusterMatrices() { if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform jointTransform(jointPose.rot(), glm::vec3(jointPose.scale()), jointPose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 16e08c2b23..cdd10a560a 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -298,9 +298,9 @@ public: TransformDualQuaternion() {} TransformDualQuaternion(const glm::mat4& m) { AnimPose p(m); - _scale.x = p.scale().x; - _scale.y = p.scale().y; - _scale.z = p.scale().z; + _scale.x = p.scale(); + _scale.y = p.scale(); + _scale.z = p.scale(); _scale.w = 0.0f; _dq = DualQuaternion(p.rot(), p.trans()); } diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 0cd9571e22..2a49846b6b 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -22,9 +22,11 @@ #include #include #include +#include QTEST_MAIN(AnimTests) + const float TEST_EPSILON = 0.001f; void AnimTests::initTestCase() { @@ -372,16 +374,10 @@ void AnimTests::testAnimPose() { const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); - std::vector scaleVec = { - glm::vec3(1), - glm::vec3(2.0f, 1.0f, 1.0f), - glm::vec3(1.0f, 0.5f, 1.0f), - glm::vec3(1.0f, 1.0f, 1.5f), - glm::vec3(2.0f, 0.5f, 1.5f), - glm::vec3(-2.0f, 0.5f, 1.5f), - glm::vec3(2.0f, -0.5f, 1.5f), - glm::vec3(2.0f, 0.5f, -1.5f), - glm::vec3(-2.0f, -0.5f, -1.5f), + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f }; std::vector rotVec = { @@ -411,7 +407,7 @@ void AnimTests::testAnimPose() { for (auto& trans : transVec) { // build a matrix the old fashioned way. - glm::mat4 scaleMat = glm::scale(glm::mat4(), scale); + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); glm::mat4 rawMat = rotTransMat * scaleMat; @@ -429,7 +425,7 @@ void AnimTests::testAnimPose() { for (auto& trans : transVec) { // build a matrix the old fashioned way. - glm::mat4 scaleMat = glm::scale(glm::mat4(), scale); + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); glm::mat4 rawMat = rotTransMat * scaleMat; @@ -445,6 +441,91 @@ void AnimTests::testAnimPose() { } } +void AnimTests::testAnimPoseMultiply() { + const float PI = (float)M_PI; + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f, + }; + + std::vector rotVec = { + glm::quat(), + ROT_X_90, + ROT_Y_180, + ROT_Z_30, + ROT_X_90 * ROT_Y_180 * ROT_Z_30, + -ROT_Y_180 + }; + + std::vector transVec = { + glm::vec3(), + glm::vec3(10.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 5.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 7.5f), + glm::vec3(10.0f, 5.0f, 7.5f), + glm::vec3(-10.0f, 5.0f, 7.5f), + glm::vec3(10.0f, -5.0f, 7.5f), + glm::vec3(10.0f, 5.0f, -7.5f) + }; + + const float TEST_EPSILON = 0.001f; + + std::vector matrixVec; + std::vector poseVec; + + for (auto& scale : scaleVec) { + for (auto& rot : rotVec) { + for (auto& trans : transVec) { + + // build a matrix the old fashioned way. + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); + glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); + glm::mat4 rawMat = rotTransMat * scaleMat; + + matrixVec.push_back(rawMat); + + // use an anim pose to build a matrix by parts. + AnimPose pose(scale, rot, trans); + poseVec.push_back(pose); + } + } + } + + for (int i = 0; i < matrixVec.size(); i++) { + for (int j = 0; j < matrixVec.size(); j++) { + + // multiply the matrices together + glm::mat4 matrix = matrixVec[i] * matrixVec[j]; + + // convert to matrix (note this will remove sheer from the matrix) + AnimPose resultA(matrix); + + // multiply the poses together directly + AnimPose resultB = poseVec[i] * poseVec[j]; + + /* + qDebug() << "matrixVec[" << i << "] =" << matrixVec[i]; + qDebug() << "matrixVec[" << j << "] =" << matrixVec[j]; + qDebug() << "matrixResult =" << resultA; + + qDebug() << "poseVec[" << i << "] =" << poseVec[i]; + qDebug() << "poseVec[" << j << "] =" << poseVec[j]; + qDebug() << "poseResult =" << resultB; + */ + + // compare results. + QCOMPARE_WITH_ABS_ERROR(resultA.scale(), resultB.scale(), TEST_EPSILON); + QCOMPARE_WITH_ABS_ERROR(resultA.rot(), resultB.rot(), TEST_EPSILON); + QCOMPARE_WITH_ABS_ERROR(resultA.trans(), resultB.trans(), TEST_EPSILON); + } + } +} + void AnimTests::testExpressionTokenizer() { QString str = "(10 + x) >= 20.1 && (y != !z)"; AnimExpression e("x"); diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 439793f21d..637611e8c4 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -27,6 +27,7 @@ private slots: void testVariant(); void testAccumulateTime(); void testAnimPose(); + void testAnimPoseMultiply(); void testExpressionTokenizer(); void testExpressionParser(); void testExpressionEvaluator(); From d8644a2745cccbba5ac6041bd52c6eb22ef78a77 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 15:06:29 -0800 Subject: [PATCH 088/130] Simplify isEqual computation for vectors used in Rig --- libraries/animation/src/Rig.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2fdbc8addb..6cbf881157 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -39,14 +39,13 @@ static int nextRigId = 1; static std::map rigRegistry; static std::mutex rigRegistryMutex; +static bool isEqual(const float p, float q) { + const float EPSILON = 0.00001f; + return fabsf(p - q) <= EPSILON; +} + static bool isEqual(const glm::vec3& u, const glm::vec3& v) { - const float EPSILON = 0.0001f; - float uLen = glm::length(u); - if (uLen == 0.0f) { - return glm::length(v) <= EPSILON; - } else { - return (glm::length(u - v) / uLen) <= EPSILON; - } + return isEqual(u.x, v.x) && isEqual(u.y, v.y) && isEqual(u.z, v.z); } static bool isEqual(const glm::quat& p, const glm::quat& q) { @@ -54,11 +53,6 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) { return 1.0f - fabsf(glm::dot(p, q)) <= EPSILON; } -static bool isEqual(const float p, float q) { - const float EPSILON = 0.00001f; - return fabsf(p - q) <= EPSILON; -} - #define ASSERT(cond) assert(cond) // 2 meter tall dude From f9afe6fe43e4885baefa3a0dd466a7525156ff5b Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 5 Feb 2019 16:39:03 -0800 Subject: [PATCH 089/130] Detect loudness and clipping on the raw audio input --- libraries/audio-client/src/AudioClient.cpp | 61 +++++++++++----------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b2cba2351f..e738b326e6 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1075,45 +1075,25 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { void AudioClient::handleAudioInput(QByteArray& audioBuffer) { if (!_audioPaused) { - if (_muted) { - _lastInputLoudness = 0.0f; - _timeSinceLastClip = 0.0f; - } else { + + bool audioGateOpen = false; + + if (!_muted) { int16_t* samples = reinterpret_cast(audioBuffer.data()); int numSamples = audioBuffer.size() / AudioConstants::SAMPLE_SIZE; int numFrames = numSamples / (_isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); if (_isNoiseGateEnabled) { // The audio gate includes DC removal - _audioGate->render(samples, samples, numFrames); + audioGateOpen = _audioGate->render(samples, samples, numFrames); } else { - _audioGate->removeDC(samples, samples, numFrames); - } - - int32_t loudness = 0; - assert(numSamples < 65536); // int32_t loudness cannot overflow - bool didClip = false; - for (int i = 0; i < numSamples; ++i) { - const int32_t CLIPPING_THRESHOLD = (int32_t)(AudioConstants::MAX_SAMPLE_VALUE * 0.9f); - int32_t sample = std::abs((int32_t)samples[i]); - loudness += sample; - didClip |= (sample > CLIPPING_THRESHOLD); - } - _lastInputLoudness = (float)loudness / numSamples; - - if (didClip) { - _timeSinceLastClip = 0.0f; - } else if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; + audioGateOpen = _audioGate->removeDC(samples, samples, numFrames); } emit inputReceived(audioBuffer); } - emit inputLoudnessChanged(_lastInputLoudness); - - // state machine to detect gate opening and closing - bool audioGateOpen = (_lastInputLoudness != 0.0f); + // detect gate opening and closing bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed _audioGateOpen = audioGateOpen; @@ -1186,10 +1166,29 @@ void AudioClient::handleMicAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - if (_muted) { - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); - } else { - _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); + + _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); + + // detect loudness and clipping on the raw input + int32_t loudness = 0; + bool didClip = false; + for (int i = 0; i < inputSamplesRequired; ++i) { + const int32_t CLIPPING_THRESHOLD = (int32_t)(AudioConstants::MAX_SAMPLE_VALUE * 0.9f); + int32_t sample = std::abs((int32_t)inputAudioSamples.get()[i]); + loudness += sample; + didClip |= (sample > CLIPPING_THRESHOLD); + } + _lastInputLoudness = (float)loudness / inputSamplesRequired; + + if (didClip) { + _timeSinceLastClip = 0.0f; + } else if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += AudioConstants::NETWORK_FRAME_SECS; + } + + emit inputLoudnessChanged(_lastInputLoudness); + + if (!_muted) { possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, From 16ef30ced0fb7bc9ec0619b1e6649e2d4d701dc8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 5 Feb 2019 17:14:25 -0800 Subject: [PATCH 090/130] don't flushRepeatedMessages() in LogHandler dtor --- libraries/shared/src/LogHandler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index 65651373be..c51d9bf611 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -38,7 +38,6 @@ LogHandler::LogHandler() { } LogHandler::~LogHandler() { - flushRepeatedMessages(); } const char* stringForLogType(LogMsgType msgType) { From a959d695545b30843c058c1d561eab08f10d070f Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 18:10:32 -0800 Subject: [PATCH 091/130] Make AnimSkeleton::getParentIndex() more cache coherent --- libraries/animation/src/AnimSkeleton.cpp | 27 +++++++++++++----------- libraries/animation/src/AnimSkeleton.h | 6 +++++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index cc48308f17..844ad5aef4 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -76,7 +76,7 @@ int AnimSkeleton::getChainDepth(int jointIndex) const { int index = jointIndex; do { chainDepth++; - index = _joints[index].parentIndex; + index = _parentIndices[index]; } while (index != -1); return chainDepth; } else { @@ -102,17 +102,12 @@ const AnimPose& AnimSkeleton::getPostRotationPose(int jointIndex) const { return _relativePostRotationPoses[jointIndex]; } -int AnimSkeleton::getParentIndex(int jointIndex) const { - return _joints[jointIndex].parentIndex; -} - std::vector AnimSkeleton::getChildrenOfJoint(int jointIndex) const { // Children and grandchildren, etc. std::vector result; if (jointIndex != -1) { - for (int i = jointIndex + 1; i < (int)_joints.size(); i++) { - if (_joints[i].parentIndex == jointIndex - || (std::find(result.begin(), result.end(), _joints[i].parentIndex) != result.end())) { + for (int i = jointIndex + 1; i < (int)_parentIndices.size(); i++) { + if (_parentIndices[i] == jointIndex || (std::find(result.begin(), result.end(), _parentIndices[i]) != result.end())) { result.push_back(i); } } @@ -128,7 +123,7 @@ AnimPose AnimSkeleton::getAbsolutePose(int jointIndex, const AnimPoseVec& relati if (jointIndex < 0 || jointIndex >= (int)relativePoses.size() || jointIndex >= _jointsSize) { return AnimPose::identity; } else { - return getAbsolutePose(_joints[jointIndex].parentIndex, relativePoses) * relativePoses[jointIndex]; + return getAbsolutePose(_parentIndices[jointIndex], relativePoses) * relativePoses[jointIndex]; } } @@ -136,7 +131,7 @@ void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const { // poses start off relative and leave in absolute frame int lastIndex = std::min((int)poses.size(), _jointsSize); for (int i = 0; i < lastIndex; ++i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { poses[i] = poses[parentIndex] * poses[i]; } @@ -147,7 +142,7 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { // poses start off absolute and leave in relative frame int lastIndex = std::min((int)poses.size(), _jointsSize); for (int i = lastIndex - 1; i >= 0; --i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { poses[i] = poses[parentIndex].inverse() * poses[i]; } @@ -158,7 +153,7 @@ void AnimSkeleton::convertAbsoluteRotationsToRelative(std::vector& ro // poses start off absolute and leave in relative frame int lastIndex = std::min((int)rotations.size(), _jointsSize); for (int i = lastIndex - 1; i >= 0; --i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { rotations[i] = glm::inverse(rotations[parentIndex]) * rotations[i]; } @@ -197,6 +192,14 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets) { _joints = joints; + + // build a seperate vector of parentIndices for cache coherency + // AnimSkeleton::getParentIndex is called very frequently in tight loops. + _parentIndices.reserve(_joints.size()); + for (auto& joint : _joints) { + _parentIndices.push_back(joint.parentIndex); + } + _jointsSize = (int)joints.size(); // build a cache of bind poses diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 14f39eedbc..0eefbf973e 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -43,7 +43,10 @@ public: // get post transform which might include FBX offset transformations const AnimPose& getPostRotationPose(int jointIndex) const; - int getParentIndex(int jointIndex) const; + int getParentIndex(int jointIndex) const { + return _parentIndices[jointIndex]; + } + std::vector getChildrenOfJoint(int jointIndex) const; AnimPose getAbsolutePose(int jointIndex, const AnimPoseVec& relativePoses) const; @@ -69,6 +72,7 @@ protected: void buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets); std::vector _joints; + std::vector _parentIndices; int _jointsSize { 0 }; AnimPoseVec _relativeDefaultPoses; AnimPoseVec _absoluteDefaultPoses; From 708309fa63e5201f1695dcd054e2cc7973c1ffae Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 18:11:30 -0800 Subject: [PATCH 092/130] CubicHermiteSplineWithArcLength optimization --- libraries/shared/src/CubicHermiteSpline.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/shared/src/CubicHermiteSpline.h b/libraries/shared/src/CubicHermiteSpline.h index da2ed26de4..cdbc64308d 100644 --- a/libraries/shared/src/CubicHermiteSpline.h +++ b/libraries/shared/src/CubicHermiteSpline.h @@ -60,7 +60,7 @@ protected: class CubicHermiteSplineFunctorWithArcLength : public CubicHermiteSplineFunctor { public: - enum Constants { NUM_SUBDIVISIONS = 30 }; + enum Constants { NUM_SUBDIVISIONS = 15 }; CubicHermiteSplineFunctorWithArcLength() : CubicHermiteSplineFunctor() { memset(_values, 0, sizeof(float) * (NUM_SUBDIVISIONS + 1)); @@ -71,11 +71,13 @@ public: float alpha = 0.0f; float accum = 0.0f; _values[0] = 0.0f; + glm::vec3 prevValue = this->operator()(alpha); for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) { - accum += glm::distance(this->operator()(alpha), - this->operator()(alpha + DELTA)); + glm::vec3 nextValue = this->operator()(alpha + DELTA); + accum += glm::distance(prevValue, nextValue); alpha += DELTA; _values[i] = accum; + prevValue = nextValue; } } From 87498b3dd2ee7bf63886bb3d8f7c18e85be43888 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 18:15:05 -0800 Subject: [PATCH 093/130] Avoid dynamic_cast in getAnimInverseKinematicsNode --- libraries/animation/src/Rig.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6cbf881157..d09de36a14 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -493,10 +493,8 @@ std::shared_ptr Rig::getAnimInverseKinematicsNode() const std::shared_ptr result; if (_animNode) { _animNode->traverse([&](AnimNode::Pointer node) { - // only report clip nodes as valid roles. - auto ikNode = std::dynamic_pointer_cast(node); - if (ikNode) { - result = ikNode; + if (node->getType() == AnimNodeType::InverseKinematics) { + result = std::dynamic_pointer_cast(node); return false; } else { return true; From 5c7e81584c204a3556e65073abb82f342b655540 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 5 Feb 2019 18:23:58 -0800 Subject: [PATCH 094/130] AnimPose::inverse() optimization --- libraries/animation/src/AnimPose.cpp | 5 ++- tests/animation/src/AnimTests.cpp | 54 ++++++++++++++++++++++++++++ tests/animation/src/AnimTests.h | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index 7b80e96ed5..366d863c3d 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -51,7 +51,10 @@ AnimPose AnimPose::operator*(const AnimPose& rhs) const { } AnimPose AnimPose::inverse() const { - return AnimPose(glm::inverse(static_cast(*this))); + float invScale = 1.0f / _scale; + glm::quat invRot = glm::inverse(_rot); + glm::vec3 invTrans = invScale * (invRot * -_trans); + return AnimPose(invScale, invRot, invTrans); } // mirror about x-axis without applying negative scale. diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 2a49846b6b..b1926efb71 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -526,6 +526,60 @@ void AnimTests::testAnimPoseMultiply() { } } +void AnimTests::testAnimPoseInverse() { + const float PI = (float)M_PI; + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f + }; + + std::vector rotVec = { + glm::quat(), + ROT_X_90, + ROT_Y_180, + ROT_Z_30, + ROT_X_90 * ROT_Y_180 * ROT_Z_30, + -ROT_Y_180 + }; + + std::vector transVec = { + glm::vec3(), + glm::vec3(10.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 5.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 7.5f), + glm::vec3(10.0f, 5.0f, 7.5f), + glm::vec3(-10.0f, 5.0f, 7.5f), + glm::vec3(10.0f, -5.0f, 7.5f), + glm::vec3(10.0f, 5.0f, -7.5f) + }; + + const float TEST_EPSILON = 0.001f; + + for (auto& scale : scaleVec) { + for (auto& rot : rotVec) { + for (auto& trans : transVec) { + + // build a matrix the old fashioned way. + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); + glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); + glm::mat4 rawMat = glm::inverse(rotTransMat * scaleMat); + + // use an anim pose to build a matrix by parts. + AnimPose pose(scale, rot, trans); + glm::mat4 poseMat = pose.inverse(); + + QCOMPARE_WITH_ABS_ERROR(rawMat, poseMat, TEST_EPSILON); + } + } + } +} + + void AnimTests::testExpressionTokenizer() { QString str = "(10 + x) >= 20.1 && (y != !z)"; AnimExpression e("x"); diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 637611e8c4..326545b0a9 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -28,6 +28,7 @@ private slots: void testAccumulateTime(); void testAnimPose(); void testAnimPoseMultiply(); + void testAnimPoseInverse(); void testExpressionTokenizer(); void testExpressionParser(); void testExpressionEvaluator(); From 2247dd3471c07288a51f1ed8e7fd6342c4f8a91e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 6 Feb 2019 18:24:40 +0100 Subject: [PATCH 095/130] local compare in entity list sorting --- scripts/system/html/js/entityList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index f059b91e81..b19873a049 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -681,6 +681,9 @@ function loaded() { if (isNullOrEmpty(valueB)) { return (isDefaultSort ? -1 : 1) * (isAscendingSort ? 1 : -1); } + if (typeof(valueA) === "string") { + return valueA.localeCompare(valueB); + } return valueA < valueB ? -1 : 1; }); }); From 4c9fb168c73a0353c9862034259f2827ce1d579b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Feb 2019 10:55:36 -0800 Subject: [PATCH 096/130] enable backface culling on procedurals --- libraries/procedural/src/procedural/Procedural.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 05cbde374d..ff8c270371 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -104,13 +104,13 @@ void ProceduralData::parse(const QJsonObject& proceduralData) { //} Procedural::Procedural() { - _opaqueState->setCullMode(gpu::State::CULL_BACK); + _opaqueState->setCullMode(gpu::State::CULL_NONE); _opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); _opaqueState->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - _transparentState->setCullMode(gpu::State::CULL_BACK); + _transparentState->setCullMode(gpu::State::CULL_NONE); _transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); _transparentState->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, From d0fecac0d83d1a1d8991452e09524113a1c638f2 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Wed, 6 Feb 2019 11:22:23 -0800 Subject: [PATCH 097/130] +android -> +android_interface --- CMakeLists.txt | 1 + .../backward.svg | 0 .../bubble-a.svg | 0 .../bubble-i.svg | 0 .../button-a.svg | 0 .../button.svg | 0 .../forward.svg | 0 .../{+android => +android_interface}/go-a.svg | 0 .../{+android => +android_interface}/go-i.svg | 0 .../{+android => +android_interface}/hand.svg | 0 .../{+android => +android_interface}/hide.svg | 0 .../mic-mute-a.svg | 0 .../mic-mute-i.svg | 0 .../mic-unmute-a.svg | 0 .../mic-unmute-i.svg | 0 .../myview-a.svg | 0 .../myview-hover.svg | 0 .../myview-i.svg | 0 .../show-up.svg | 0 .../stats.svg | 0 .../{+android => +android_interface}/tick.svg | 0 .../StatText.qml | 0 .../Stats.qml | 0 .../Web3DSurface.qml | 0 .../LinkAccountBody.qml | 0 .../SignUpBody.qml | 0 .../ImageButton.qml | 0 .../FocusHack.qml | 0 .../ActionBar.qml | 0 .../AudioBar.qml | 0 .../AvatarOption.qml | 0 .../Desktop.qml | 0 .../HifiConstants.qml | 0 .../StatsBar.qml | 0 .../WindowHeader.qml | 0 .../bottomHudOptions.qml | 0 .../button.qml | 0 .../modesbar.qml | 0 .../TransparencyMask.qml | 0 .../HifiConstants.qml | 0 libraries/shared/src/shared/FileUtils.cpp | 2 +- .../defaultScripts.js | 0 .../+android_questInterface/defaultScripts.js | 1 + .../actionbar.js | 0 .../{+android => +android_interface}/audio.js | 0 .../clickWeb.js | 0 .../displayNames.js | 0 .../{+android => +android_interface}/modes.js | 0 .../{+android => +android_interface}/radar.js | 0 .../{+android => +android_interface}/stats.js | 0 .../touchscreenvirtualpad.js | 0 .../uniqueColor.js | 0 scripts/system/quickGoto.js | 36 +++++++++++++++++++ 53 files changed, 39 insertions(+), 1 deletion(-) rename interface/resources/icons/{+android => +android_interface}/backward.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/bubble-a.svg (100%) rename interface/resources/icons/{+android => +android_interface}/bubble-i.svg (100%) rename interface/resources/icons/{+android => +android_interface}/button-a.svg (100%) rename interface/resources/icons/{+android => +android_interface}/button.svg (100%) rename interface/resources/icons/{+android => +android_interface}/forward.svg (100%) rename interface/resources/icons/{+android => +android_interface}/go-a.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/go-i.svg (100%) rename interface/resources/icons/{+android => +android_interface}/hand.svg (100%) rename interface/resources/icons/{+android => +android_interface}/hide.svg (100%) rename interface/resources/icons/{+android => +android_interface}/mic-mute-a.svg (100%) rename interface/resources/icons/{+android => +android_interface}/mic-mute-i.svg (100%) rename interface/resources/icons/{+android => +android_interface}/mic-unmute-a.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/mic-unmute-i.svg (100%) rename interface/resources/icons/{+android => +android_interface}/myview-a.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/myview-hover.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/myview-i.svg (100%) mode change 100755 => 100644 rename interface/resources/icons/{+android => +android_interface}/show-up.svg (100%) rename interface/resources/icons/{+android => +android_interface}/stats.svg (100%) rename interface/resources/icons/{+android => +android_interface}/tick.svg (100%) rename interface/resources/qml/{+android => +android_interface}/StatText.qml (100%) rename interface/resources/qml/{+android => +android_interface}/Stats.qml (100%) rename interface/resources/qml/{+android => +android_interface}/Web3DSurface.qml (100%) rename interface/resources/qml/LoginDialog/{+android => +android_interface}/LinkAccountBody.qml (100%) rename interface/resources/qml/LoginDialog/{+android => +android_interface}/SignUpBody.qml (100%) rename interface/resources/qml/controlsUit/{+android => +android_interface}/ImageButton.qml (100%) rename interface/resources/qml/desktop/{+android => +android_interface}/FocusHack.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/ActionBar.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/AudioBar.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/AvatarOption.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/Desktop.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/HifiConstants.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/StatsBar.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/WindowHeader.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/bottomHudOptions.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/button.qml (100%) rename interface/resources/qml/hifi/{+android => +android_interface}/modesbar.qml (100%) rename interface/resources/qml/hifi/avatarapp/{+android => +android_interface}/TransparencyMask.qml (100%) rename interface/resources/qml/stylesUit/{+android => +android_interface}/HifiConstants.qml (100%) rename scripts/{+android => +android_interface}/defaultScripts.js (100%) rename scripts/system/{+android => +android_interface}/actionbar.js (100%) rename scripts/system/{+android => +android_interface}/audio.js (100%) rename scripts/system/{+android => +android_interface}/clickWeb.js (100%) rename scripts/system/{+android => +android_interface}/displayNames.js (100%) rename scripts/system/{+android => +android_interface}/modes.js (100%) rename scripts/system/{+android => +android_interface}/radar.js (100%) rename scripts/system/{+android => +android_interface}/stats.js (100%) rename scripts/system/{+android => +android_interface}/touchscreenvirtualpad.js (100%) rename scripts/system/{+android => +android_interface}/uniqueColor.js (100%) create mode 100644 scripts/system/quickGoto.js diff --git a/CMakeLists.txt b/CMakeLists.txt index 6956fd22c3..4d616e1f3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ endif() if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) + add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") else () set(PLATFORM_QT_COMPONENTS WebEngine) endif () diff --git a/interface/resources/icons/+android/backward.svg b/interface/resources/icons/+android_interface/backward.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/backward.svg rename to interface/resources/icons/+android_interface/backward.svg diff --git a/interface/resources/icons/+android/bubble-a.svg b/interface/resources/icons/+android_interface/bubble-a.svg similarity index 100% rename from interface/resources/icons/+android/bubble-a.svg rename to interface/resources/icons/+android_interface/bubble-a.svg diff --git a/interface/resources/icons/+android/bubble-i.svg b/interface/resources/icons/+android_interface/bubble-i.svg similarity index 100% rename from interface/resources/icons/+android/bubble-i.svg rename to interface/resources/icons/+android_interface/bubble-i.svg diff --git a/interface/resources/icons/+android/button-a.svg b/interface/resources/icons/+android_interface/button-a.svg similarity index 100% rename from interface/resources/icons/+android/button-a.svg rename to interface/resources/icons/+android_interface/button-a.svg diff --git a/interface/resources/icons/+android/button.svg b/interface/resources/icons/+android_interface/button.svg similarity index 100% rename from interface/resources/icons/+android/button.svg rename to interface/resources/icons/+android_interface/button.svg diff --git a/interface/resources/icons/+android/forward.svg b/interface/resources/icons/+android_interface/forward.svg similarity index 100% rename from interface/resources/icons/+android/forward.svg rename to interface/resources/icons/+android_interface/forward.svg diff --git a/interface/resources/icons/+android/go-a.svg b/interface/resources/icons/+android_interface/go-a.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/go-a.svg rename to interface/resources/icons/+android_interface/go-a.svg diff --git a/interface/resources/icons/+android/go-i.svg b/interface/resources/icons/+android_interface/go-i.svg similarity index 100% rename from interface/resources/icons/+android/go-i.svg rename to interface/resources/icons/+android_interface/go-i.svg diff --git a/interface/resources/icons/+android/hand.svg b/interface/resources/icons/+android_interface/hand.svg similarity index 100% rename from interface/resources/icons/+android/hand.svg rename to interface/resources/icons/+android_interface/hand.svg diff --git a/interface/resources/icons/+android/hide.svg b/interface/resources/icons/+android_interface/hide.svg similarity index 100% rename from interface/resources/icons/+android/hide.svg rename to interface/resources/icons/+android_interface/hide.svg diff --git a/interface/resources/icons/+android/mic-mute-a.svg b/interface/resources/icons/+android_interface/mic-mute-a.svg similarity index 100% rename from interface/resources/icons/+android/mic-mute-a.svg rename to interface/resources/icons/+android_interface/mic-mute-a.svg diff --git a/interface/resources/icons/+android/mic-mute-i.svg b/interface/resources/icons/+android_interface/mic-mute-i.svg similarity index 100% rename from interface/resources/icons/+android/mic-mute-i.svg rename to interface/resources/icons/+android_interface/mic-mute-i.svg diff --git a/interface/resources/icons/+android/mic-unmute-a.svg b/interface/resources/icons/+android_interface/mic-unmute-a.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/mic-unmute-a.svg rename to interface/resources/icons/+android_interface/mic-unmute-a.svg diff --git a/interface/resources/icons/+android/mic-unmute-i.svg b/interface/resources/icons/+android_interface/mic-unmute-i.svg similarity index 100% rename from interface/resources/icons/+android/mic-unmute-i.svg rename to interface/resources/icons/+android_interface/mic-unmute-i.svg diff --git a/interface/resources/icons/+android/myview-a.svg b/interface/resources/icons/+android_interface/myview-a.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/myview-a.svg rename to interface/resources/icons/+android_interface/myview-a.svg diff --git a/interface/resources/icons/+android/myview-hover.svg b/interface/resources/icons/+android_interface/myview-hover.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/myview-hover.svg rename to interface/resources/icons/+android_interface/myview-hover.svg diff --git a/interface/resources/icons/+android/myview-i.svg b/interface/resources/icons/+android_interface/myview-i.svg old mode 100755 new mode 100644 similarity index 100% rename from interface/resources/icons/+android/myview-i.svg rename to interface/resources/icons/+android_interface/myview-i.svg diff --git a/interface/resources/icons/+android/show-up.svg b/interface/resources/icons/+android_interface/show-up.svg similarity index 100% rename from interface/resources/icons/+android/show-up.svg rename to interface/resources/icons/+android_interface/show-up.svg diff --git a/interface/resources/icons/+android/stats.svg b/interface/resources/icons/+android_interface/stats.svg similarity index 100% rename from interface/resources/icons/+android/stats.svg rename to interface/resources/icons/+android_interface/stats.svg diff --git a/interface/resources/icons/+android/tick.svg b/interface/resources/icons/+android_interface/tick.svg similarity index 100% rename from interface/resources/icons/+android/tick.svg rename to interface/resources/icons/+android_interface/tick.svg diff --git a/interface/resources/qml/+android/StatText.qml b/interface/resources/qml/+android_interface/StatText.qml similarity index 100% rename from interface/resources/qml/+android/StatText.qml rename to interface/resources/qml/+android_interface/StatText.qml diff --git a/interface/resources/qml/+android/Stats.qml b/interface/resources/qml/+android_interface/Stats.qml similarity index 100% rename from interface/resources/qml/+android/Stats.qml rename to interface/resources/qml/+android_interface/Stats.qml diff --git a/interface/resources/qml/+android/Web3DSurface.qml b/interface/resources/qml/+android_interface/Web3DSurface.qml similarity index 100% rename from interface/resources/qml/+android/Web3DSurface.qml rename to interface/resources/qml/+android_interface/Web3DSurface.qml diff --git a/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml similarity index 100% rename from interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml rename to interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml diff --git a/interface/resources/qml/LoginDialog/+android/SignUpBody.qml b/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml similarity index 100% rename from interface/resources/qml/LoginDialog/+android/SignUpBody.qml rename to interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml diff --git a/interface/resources/qml/controlsUit/+android/ImageButton.qml b/interface/resources/qml/controlsUit/+android_interface/ImageButton.qml similarity index 100% rename from interface/resources/qml/controlsUit/+android/ImageButton.qml rename to interface/resources/qml/controlsUit/+android_interface/ImageButton.qml diff --git a/interface/resources/qml/desktop/+android/FocusHack.qml b/interface/resources/qml/desktop/+android_interface/FocusHack.qml similarity index 100% rename from interface/resources/qml/desktop/+android/FocusHack.qml rename to interface/resources/qml/desktop/+android_interface/FocusHack.qml diff --git a/interface/resources/qml/hifi/+android/ActionBar.qml b/interface/resources/qml/hifi/+android_interface/ActionBar.qml similarity index 100% rename from interface/resources/qml/hifi/+android/ActionBar.qml rename to interface/resources/qml/hifi/+android_interface/ActionBar.qml diff --git a/interface/resources/qml/hifi/+android/AudioBar.qml b/interface/resources/qml/hifi/+android_interface/AudioBar.qml similarity index 100% rename from interface/resources/qml/hifi/+android/AudioBar.qml rename to interface/resources/qml/hifi/+android_interface/AudioBar.qml diff --git a/interface/resources/qml/hifi/+android/AvatarOption.qml b/interface/resources/qml/hifi/+android_interface/AvatarOption.qml similarity index 100% rename from interface/resources/qml/hifi/+android/AvatarOption.qml rename to interface/resources/qml/hifi/+android_interface/AvatarOption.qml diff --git a/interface/resources/qml/hifi/+android/Desktop.qml b/interface/resources/qml/hifi/+android_interface/Desktop.qml similarity index 100% rename from interface/resources/qml/hifi/+android/Desktop.qml rename to interface/resources/qml/hifi/+android_interface/Desktop.qml diff --git a/interface/resources/qml/hifi/+android/HifiConstants.qml b/interface/resources/qml/hifi/+android_interface/HifiConstants.qml similarity index 100% rename from interface/resources/qml/hifi/+android/HifiConstants.qml rename to interface/resources/qml/hifi/+android_interface/HifiConstants.qml diff --git a/interface/resources/qml/hifi/+android/StatsBar.qml b/interface/resources/qml/hifi/+android_interface/StatsBar.qml similarity index 100% rename from interface/resources/qml/hifi/+android/StatsBar.qml rename to interface/resources/qml/hifi/+android_interface/StatsBar.qml diff --git a/interface/resources/qml/hifi/+android/WindowHeader.qml b/interface/resources/qml/hifi/+android_interface/WindowHeader.qml similarity index 100% rename from interface/resources/qml/hifi/+android/WindowHeader.qml rename to interface/resources/qml/hifi/+android_interface/WindowHeader.qml diff --git a/interface/resources/qml/hifi/+android/bottomHudOptions.qml b/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml similarity index 100% rename from interface/resources/qml/hifi/+android/bottomHudOptions.qml rename to interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml diff --git a/interface/resources/qml/hifi/+android/button.qml b/interface/resources/qml/hifi/+android_interface/button.qml similarity index 100% rename from interface/resources/qml/hifi/+android/button.qml rename to interface/resources/qml/hifi/+android_interface/button.qml diff --git a/interface/resources/qml/hifi/+android/modesbar.qml b/interface/resources/qml/hifi/+android_interface/modesbar.qml similarity index 100% rename from interface/resources/qml/hifi/+android/modesbar.qml rename to interface/resources/qml/hifi/+android_interface/modesbar.qml diff --git a/interface/resources/qml/hifi/avatarapp/+android/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/+android_interface/TransparencyMask.qml similarity index 100% rename from interface/resources/qml/hifi/avatarapp/+android/TransparencyMask.qml rename to interface/resources/qml/hifi/avatarapp/+android_interface/TransparencyMask.qml diff --git a/interface/resources/qml/stylesUit/+android/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml similarity index 100% rename from interface/resources/qml/stylesUit/+android/HifiConstants.qml rename to interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 9a58fc3e78..f2a4925351 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -32,7 +32,7 @@ const QStringList& FileUtils::getFileSelectors() { std::call_once(once, [] { #if defined(Q_OS_ANDROID) - //extraSelectors << "android_" HIFI_ANDROID_APP; + extraSelectors << "android_" HIFI_ANDROID_APP; #endif #if defined(USE_GLES) diff --git a/scripts/+android/defaultScripts.js b/scripts/+android_interface/defaultScripts.js similarity index 100% rename from scripts/+android/defaultScripts.js rename to scripts/+android_interface/defaultScripts.js diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js index da50e4a182..d22716302c 100644 --- a/scripts/+android_questInterface/defaultScripts.js +++ b/scripts/+android_questInterface/defaultScripts.js @@ -25,6 +25,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/notifications.js", "system/commerce/wallet.js", "system/dialTone.js", + "system/quickGoto.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/miniTablet.js" diff --git a/scripts/system/+android/actionbar.js b/scripts/system/+android_interface/actionbar.js similarity index 100% rename from scripts/system/+android/actionbar.js rename to scripts/system/+android_interface/actionbar.js diff --git a/scripts/system/+android/audio.js b/scripts/system/+android_interface/audio.js similarity index 100% rename from scripts/system/+android/audio.js rename to scripts/system/+android_interface/audio.js diff --git a/scripts/system/+android/clickWeb.js b/scripts/system/+android_interface/clickWeb.js similarity index 100% rename from scripts/system/+android/clickWeb.js rename to scripts/system/+android_interface/clickWeb.js diff --git a/scripts/system/+android/displayNames.js b/scripts/system/+android_interface/displayNames.js similarity index 100% rename from scripts/system/+android/displayNames.js rename to scripts/system/+android_interface/displayNames.js diff --git a/scripts/system/+android/modes.js b/scripts/system/+android_interface/modes.js similarity index 100% rename from scripts/system/+android/modes.js rename to scripts/system/+android_interface/modes.js diff --git a/scripts/system/+android/radar.js b/scripts/system/+android_interface/radar.js similarity index 100% rename from scripts/system/+android/radar.js rename to scripts/system/+android_interface/radar.js diff --git a/scripts/system/+android/stats.js b/scripts/system/+android_interface/stats.js similarity index 100% rename from scripts/system/+android/stats.js rename to scripts/system/+android_interface/stats.js diff --git a/scripts/system/+android/touchscreenvirtualpad.js b/scripts/system/+android_interface/touchscreenvirtualpad.js similarity index 100% rename from scripts/system/+android/touchscreenvirtualpad.js rename to scripts/system/+android_interface/touchscreenvirtualpad.js diff --git a/scripts/system/+android/uniqueColor.js b/scripts/system/+android_interface/uniqueColor.js similarity index 100% rename from scripts/system/+android/uniqueColor.js rename to scripts/system/+android_interface/uniqueColor.js diff --git a/scripts/system/quickGoto.js b/scripts/system/quickGoto.js new file mode 100644 index 0000000000..c5560cce83 --- /dev/null +++ b/scripts/system/quickGoto.js @@ -0,0 +1,36 @@ +"use strict"; + +// +// quickGoto.js +// scripts/system/ +// +// Created by Dante Ruiz +// Copyright 2016 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 +// +/* globals Tablet, Toolbars, Script, HMD, DialogsManager */ + +(function() { // BEGIN LOCAL_SCOPE + + function addGotoButton(destination) { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", + text: destination + }); + var buttonDestination = destination; + button.clicked.connect(function() { + Window.location = "hifi://" + buttonDestination; + }); + Script.scriptEnding.connect(function () { + tablet.removeButton(button); + }); + } + + addGotoButton("dev-mobile"); + addGotoButton("quest-dev"); + +}()); // END LOCAL_SCOPE From f07b5c8490aa43d885e7ae4ffc99143e0c8cda1b Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 6 Feb 2019 14:15:14 -0800 Subject: [PATCH 098/130] update urls --- scripts/+android_interface/defaultScripts.js | 10 +++++----- scripts/system/+android_interface/actionbar.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/+android_interface/defaultScripts.js b/scripts/+android_interface/defaultScripts.js index 8950af808d..e6971f5a6b 100644 --- a/scripts/+android_interface/defaultScripts.js +++ b/scripts/+android_interface/defaultScripts.js @@ -13,10 +13,10 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", - "system/+android/touchscreenvirtualpad.js", - "system/+android/actionbar.js", - "system/+android/audio.js" , - "system/+android/modes.js"/*, + "system/+android_interface/touchscreenvirtualpad.js", + "system/+android_interface/actionbar.js", + "system/+android_interface/audio.js" , + "system/+android_interface/modes.js"/*, "system/away.js", "system/controllers/controllerDisplayManager.js", "system/controllers/handControllerGrabAndroid.js", @@ -33,7 +33,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ ]; var DEBUG_SCRIPTS = [ - "system/+android/stats.js" + "system/+android_interface/stats.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ ]; diff --git a/scripts/system/+android_interface/actionbar.js b/scripts/system/+android_interface/actionbar.js index e7e2459e69..74b3896a62 100644 --- a/scripts/system/+android_interface/actionbar.js +++ b/scripts/system/+android_interface/actionbar.js @@ -26,8 +26,8 @@ function init() { qml: "hifi/ActionBar.qml" }); backButton = actionbar.addButton({ - icon: "icons/+android/backward.svg", - activeIcon: "icons/+android/backward.svg", + icon: "icons/+android_interface/backward.svg", + activeIcon: "icons/+android_interface/backward.svg", text: "", bgOpacity: 0.0, hoverBgOpacity: 0.0, From 8f09c5b689c1c7ccff2a65b4618f85f49a8ae232 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 6 Feb 2019 14:28:19 -0800 Subject: [PATCH 099/130] Updated readme. --- tools/nitpick/README.md | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index b027f222d3..0f0f715d01 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -20,34 +20,46 @@ Nitpick has 5 functions, separated into separate tabs: 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) 1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ - 1. Open a new command prompt and run `aws configure` + 1. Open a new command prompt and run + `aws configure` 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] - 1. Install the latest release of Boto3 via pip: `pip install boto3` + 1. Install the latest release of Boto3 via pip: + `pip install boto3` -1. (First time) Install adb (Android Debug Bridge) from `https://dl.google.com/android/repository/platform-tools-latest-windows.zip` - 1. Create and environment variable named ADB_PATH and set its value to the installation location (e.g. **C:\adb**) +1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip* + 1. Copy the downloaded file to (for example) **C:\adb** and extract in place. + Verify you see *adb.exe* in **C:\adb\platform-tools\\**. + 1. Create an environment variable named ADB_PATH and set its value to the installation location (e.g. **C:\adb**) ### Mac 1. (first time) Install brew - In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` + In a terminal: + `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` 1. (First time) install Qt: - In a terminal: `brew install qt` -1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (**macOS 64-bit installer** or **macOS 64-bit/32-bit installer**) - 1. After installation - In a terminal: run `open "/Applications/Python 3.6/Install Certificates.command"`. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. + In a terminal: +`brew install qt` +1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (*macOS 64-bit installer* or *macOS 64-bit/32-bit installer*) + 1. After installation - In a terminal: run + `open "/Applications/Python 3.7/Install Certificates.command"`. +This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. 1. Verify that `/usr/local/bin/python3` exists. 1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority: -In a terminal: `curl -O https://bootstrap.pypa.io/get-pip.py` -In a terminal: `python3 get-pip.py --user` + In a terminal: + `curl -O https://bootstrap.pypa.io/get-pip.py` + In a terminal: + `python3 get-pip.py --user` 1. Use pip to install the AWS CLI. - `pip3 install awscli --upgrade --user` + `pip3 install awscli --upgrade --user` This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin - 1. Open a new command prompt and run `~/Library/Python/3.7/bin/aws configure` + 1. Open a new command prompt and run + `~/Library/Python/3.7/bin/aws configure` 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] 1. Install the latest release of Boto3 via pip: pip3 install boto3 -1. (First time)Install adb (Android Debug Bridge) from `https://dl.google.com/android/repository/platform-tools-latest-darwin.zip` +1. (First time)Install adb (the Android Debug Bridge) - in a terminal: + `brew cask install android-platform-tools` # Usage ## Create ![](./Create.PNG) From 918e2fc7ab7a3f665ef8d34903602b8a6f7a00f4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 6 Feb 2019 16:08:30 -0800 Subject: [PATCH 100/130] Proper detection of digital clipping on the unprocessed audio input. Triggered when 3+ consecutive samples > -0.1 dBFS. --- libraries/audio-client/src/AudioClient.cpp | 64 ++++++++++++++++++---- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index e738b326e6..cd1363a84d 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -170,6 +170,57 @@ static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { } } +static float computeLoudness(int16_t* samples, int numSamples, int numChannels, bool& isClipping) { + + const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS + const int32_t CLIPPING_DETECTION = 3; // consecutive samples over threshold + + float scale = numSamples ? 1.0f / numSamples : 0.0f; + + int32_t loudness = 0; + isClipping = false; + + if (numChannels == 2) { + int32_t oversLeft = 0; + int32_t oversRight = 0; + + for (int i = 0; i < numSamples/2; i++) { + int32_t left = std::abs((int32_t)samples[2*i+0]); + int32_t right = std::abs((int32_t)samples[2*i+1]); + + loudness += left; + loudness += right; + + if (left > CLIPPING_THRESHOLD) { + isClipping |= (++oversLeft >= CLIPPING_DETECTION); + } else { + oversLeft = 0; + } + if (right > CLIPPING_THRESHOLD) { + isClipping |= (++oversRight >= CLIPPING_DETECTION); + } else { + oversRight = 0; + } + } + } else { + int32_t overs = 0; + + for (int i = 0; i < numSamples; i++) { + int32_t sample = std::abs((int32_t)samples[i]); + + loudness += sample; + + if (sample > CLIPPING_THRESHOLD) { + isClipping |= (++overs >= CLIPPING_DETECTION); + } else { + overs = 0; + } + } + } + + return (float)loudness * scale; +} + static inline float convertToFloat(int16_t sample) { return (float)sample * (1 / 32768.0f); } @@ -1170,17 +1221,10 @@ void AudioClient::handleMicAudioInput() { _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); // detect loudness and clipping on the raw input - int32_t loudness = 0; - bool didClip = false; - for (int i = 0; i < inputSamplesRequired; ++i) { - const int32_t CLIPPING_THRESHOLD = (int32_t)(AudioConstants::MAX_SAMPLE_VALUE * 0.9f); - int32_t sample = std::abs((int32_t)inputAudioSamples.get()[i]); - loudness += sample; - didClip |= (sample > CLIPPING_THRESHOLD); - } - _lastInputLoudness = (float)loudness / inputSamplesRequired; + bool isClipping = false; + _lastInputLoudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); - if (didClip) { + if (isClipping) { _timeSinceLastClip = 0.0f; } else if (_timeSinceLastClip >= 0.0f) { _timeSinceLastClip += AudioConstants::NETWORK_FRAME_SECS; From ba00f95f72f05d6eb38c9d4ab51b8bc3a1e78d6c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 6 Feb 2019 18:23:50 -0800 Subject: [PATCH 101/130] Expose clipping status to audio scripting interface --- interface/src/scripting/Audio.cpp | 23 ++++++++++++++++++---- interface/src/scripting/Audio.h | 14 ++++++++++++- libraries/audio-client/src/AudioClient.cpp | 3 ++- libraries/audio-client/src/AudioClient.h | 2 +- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 524170c7a5..9ccb698da0 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -150,18 +150,33 @@ float Audio::getInputLevel() const { }); } -void Audio::onInputLoudnessChanged(float loudness) { +bool Audio::isClipping() const { + return resultWithReadLock([&] { + return _isClipping; + }); +} + +void Audio::onInputLoudnessChanged(float loudness, bool isClipping) { float level = loudnessToLevel(loudness); - bool changed = false; + bool levelChanged = false; + bool isClippingChanged = false; + withWriteLock([&] { if (_inputLevel != level) { _inputLevel = level; - changed = true; + levelChanged = true; + } + if (_isClipping != isClipping) { + _isClipping = isClipping; + isClippingChanged = true; } }); - if (changed) { + if (levelChanged) { emit inputLevelChanged(level); } + if (isClippingChanged) { + emit clippingChanged(isClipping); + } } QString Audio::getContext() const { diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 4c4bf6dd60..63758d0633 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -41,6 +41,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * above the noise floor. * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – * 1.0 (the onset of clipping). Read-only. + * @property {boolean} clipping - true if the audio input is clipping, otherwise false. * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some * devices, and others might only support values of 0.0 and 1.0. @@ -58,6 +59,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) + Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) @@ -74,6 +76,7 @@ public: bool noiseReductionEnabled() const; float getInputVolume() const; float getInputLevel() const; + bool isClipping() const; QString getContext() const; void showMicMeter(bool show); @@ -217,6 +220,14 @@ signals: */ void inputLevelChanged(float level); + /**jsdoc + * Triggered when the clipping state of the input audio changes. + * @function Audio.clippingChanged + * @param {boolean} isClipping - true if the audio input is clipping, otherwise false. + * @returns {Signal} + */ + void clippingChanged(bool isClipping); + /**jsdoc * Triggered when the current context of the audio changes. * @function Audio.contextChanged @@ -237,7 +248,7 @@ private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); void setInputVolume(float volume); - void onInputLoudnessChanged(float loudness); + void onInputLoudnessChanged(float loudness, bool isClipping); protected: // Audio must live on a separate thread from AudioClient to avoid deadlocks @@ -247,6 +258,7 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + bool _isClipping { false }; bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index cd1363a84d..c9cf1646ec 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1229,8 +1229,9 @@ void AudioClient::handleMicAudioInput() { } else if (_timeSinceLastClip >= 0.0f) { _timeSinceLastClip += AudioConstants::NETWORK_FRAME_SECS; } + isClipping = (_timeSinceLastClip >= 0.0f) && (_timeSinceLastClip < 2.0f); // 2 second hold time - emit inputLoudnessChanged(_lastInputLoudness); + emit inputLoudnessChanged(_lastInputLoudness, isClipping); if (!_muted) { possibleResampling(_inputToNetworkResampler, diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index f3e1ad9a52..94ed2ce132 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -248,7 +248,7 @@ signals: void noiseReductionChanged(bool noiseReductionEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); - void inputLoudnessChanged(float loudness); + void inputLoudnessChanged(float loudness, bool isClipping); void outputBytesToNetwork(int numBytes); void inputBytesFromNetwork(int numBytes); void noiseGateOpened(); From 7fe0e5909e7ef4e13eaeb8cfe7b94e5c40f36465 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 7 Feb 2019 10:14:10 -0800 Subject: [PATCH 102/130] fix black albedo coloring --- libraries/graphics/src/graphics/Material.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp index 7befb7e053..7743c4bf50 100755 --- a/libraries/graphics/src/graphics/Material.cpp +++ b/libraries/graphics/src/graphics/Material.cpp @@ -87,7 +87,7 @@ void Material::setUnlit(bool value) { } void Material::setAlbedo(const glm::vec3& albedo, bool isSRGB) { - _key.setAlbedo(glm::any(glm::greaterThan(albedo, glm::vec3(0.0f)))); + _key.setAlbedo(true); _albedo = (isSRGB ? ColorUtils::sRGBToLinearVec3(albedo) : albedo); } From 985f6dcb752ff11b0a795c8004988964ec9c363a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 7 Feb 2019 11:22:33 -0800 Subject: [PATCH 103/130] Fix normal textures not being visible --- .../model-baker/src/model-baker/CalculateMeshTangentsTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index e94e15507e..c561a745b5 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -47,7 +47,7 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co break; } } - if (needTangents) { + if (!needTangents) { continue; } From ef529ed309446555a243ca9cfe5d746cb254c51c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 7 Feb 2019 11:27:08 -0800 Subject: [PATCH 104/130] Do not use continues in logic checking if we should calculate mesh tangents --- .../model-baker/CalculateMeshTangentsTask.cpp | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index c561a745b5..6e12ec546d 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -13,6 +13,17 @@ #include "ModelMath.h" +bool needTangents(const hfm::Mesh& mesh, const QHash& materials) { + // Check if we actually need to calculate the tangents + for (const auto& meshPart : mesh.parts) { + auto materialIt = materials.find(meshPart.materialID); + if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { + return true; + } + } + return false; +} + void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& normalsPerMesh = input.get0(); const std::vector& meshes = input.get1(); @@ -28,38 +39,20 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; // Check if we already have tangents and therefore do not need to do any calculation + // Otherwise confirm if we have the normals needed, and need to calculate the tangents if (!tangentsIn.empty()) { tangentsOut = tangentsIn.toStdVector(); - continue; + } else if (!normals.empty() && needTangents(mesh, materials)) { + tangentsOut.resize(normals.size()); + baker::calculateTangents(mesh, + [&mesh, &normals, &tangentsOut](int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec2* outTexCoords, glm::vec3& outNormal) { + outVertices[0] = mesh.vertices[firstIndex]; + outVertices[1] = mesh.vertices[secondIndex]; + outNormal = normals[firstIndex]; + outTexCoords[0] = mesh.texCoords[firstIndex]; + outTexCoords[1] = mesh.texCoords[secondIndex]; + return &(tangentsOut[firstIndex]); + }); } - - // Check if we have normals, and if not then tangents can't be calculated - if (normals.empty()) { - continue; - } - - // Check if we actually need to calculate the tangents - bool needTangents = false; - for (const auto& meshPart : mesh.parts) { - auto materialIt = materials.find(meshPart.materialID); - if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { - needTangents = true; - break; - } - } - if (!needTangents) { - continue; - } - - tangentsOut.resize(normals.size()); - baker::calculateTangents(mesh, - [&mesh, &normals, &tangentsOut](int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec2* outTexCoords, glm::vec3& outNormal) { - outVertices[0] = mesh.vertices[firstIndex]; - outVertices[1] = mesh.vertices[secondIndex]; - outNormal = normals[firstIndex]; - outTexCoords[0] = mesh.texCoords[firstIndex]; - outTexCoords[1] = mesh.texCoords[secondIndex]; - return &(tangentsOut[firstIndex]); - }); } } From f8608464fabcfc6c7abffde25e679f58c747cb84 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 7 Feb 2019 11:33:44 -0800 Subject: [PATCH 105/130] warning fixes --- libraries/animation/src/AnimPose.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h index 89dcbaf2ab..4d6dee1987 100644 --- a/libraries/animation/src/AnimPose.h +++ b/libraries/animation/src/AnimPose.h @@ -21,9 +21,9 @@ class AnimPose { public: AnimPose() {} explicit AnimPose(const glm::mat4& mat); - explicit AnimPose(const glm::quat& rotIn) : _scale(1.0f), _rot(rotIn), _trans(0.0f) {} - AnimPose(const glm::quat& rotIn, const glm::vec3& transIn) : _scale(1.0f), _rot(rotIn), _trans(transIn) {} - AnimPose(float scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _scale(scaleIn), _rot(rotIn), _trans(transIn) {} + explicit AnimPose(const glm::quat& rotIn) : _rot(rotIn), _trans(0.0f), _scale(1.0f) {} + AnimPose(const glm::quat& rotIn, const glm::vec3& transIn) : _rot(rotIn), _trans(transIn), _scale(1.0f) {} + AnimPose(float scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _rot(rotIn), _trans(transIn), _scale(scaleIn) {} static const AnimPose identity; glm::vec3 xformPoint(const glm::vec3& rhs) const; @@ -36,7 +36,7 @@ public: AnimPose mirror() const; operator glm::mat4() const; - const float scale() const { return _scale; } + float scale() const { return _scale; } float& scale() { return _scale; } const glm::quat& rot() const { return _rot; } From d04445c244397be01411b9b28f7002628feb6544 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 7 Feb 2019 12:56:55 -0800 Subject: [PATCH 106/130] Qml Marketplace - remove some quest incompatibilities and disable links for quest --- .../hifi/commerce/marketplace/MarketplaceItem.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 24ef528673..0a57e56099 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -14,8 +14,6 @@ import Hifi 1.0 as Hifi import QtQuick 2.9 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 -import QtWebEngine 1.5 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls @@ -424,13 +422,13 @@ Rectangle { elide: Text.ElideRight size: 14 - color: model.link ? hifi.colors.blueHighlight : hifi.colors.baseGray + color: (model.link && root.supports3DHTML)? hifi.colors.blueHighlight : hifi.colors.baseGray verticalAlignment: Text.AlignVCenter MouseArea { anchors.fill: parent onClicked: { - if (model.link) { + if (model.link && root.supports3DHTML) { sendToScript({method: 'marketplace_open_link', link: model.link}); } } @@ -571,12 +569,14 @@ Rectangle { text: root.description size: 14 color: hifi.colors.lightGray - linkColor: hifi.colors.blueHighlight + linkColor: root.supports3DHTML ? hifi.colors.blueHighlight : hifi.colors.lightGray verticalAlignment: Text.AlignVCenter textFormat: Text.RichText wrapMode: Text.Wrap onLinkActivated: { - sendToScript({method: 'marketplace_open_link', link: link}); + if (root.supports3DHTML) { + sendToScript({method: 'marketplace_open_link', link: link}); + } } onHeightChanged: { footer.evalHeight(); } From f9396fe5975343a70795311b47ca290e45636ff4 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 14:11:59 -0800 Subject: [PATCH 107/130] Corrected error message. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 10216f248c..e1c82854f4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -53,7 +53,7 @@ TestRunnerMobile::TestRunnerMobile( if (QProcessEnvironment::systemEnvironment().contains("ADB_PATH")) { QString adbExePath = QProcessEnvironment::systemEnvironment().value("ADB_PATH") + "/platform-tools"; if (!QFile::exists(adbExePath + "/" + _adbExe)) { - QMessageBox::critical(0, _adbExe, QString("Python executable not found in ") + adbExePath); + QMessageBox::critical(0, _adbExe, QString("ADB executable not found in ") + adbExePath); exit(-1); } From 03ad14caafea12e0eff587a1258332972cae0437 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 14:17:18 -0800 Subject: [PATCH 108/130] Removed unneeded step. --- tools/nitpick/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 0f0f715d01..4588f55df2 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -16,7 +16,6 @@ Nitpick has 5 functions, separated into separate tabs: ## Installation `nitpick` is packaged with High Fidelity PR and Development builds. ### Windows -1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe) 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) 1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ From bab306fd30f4bd50418dbca21b6e92784b4ea032 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 14:38:53 -0800 Subject: [PATCH 109/130] Added instruction to add Python to path. --- tools/nitpick/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 4588f55df2..2812bc71f5 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -17,6 +17,7 @@ Nitpick has 5 functions, separated into separate tabs: `nitpick` is packaged with High Fidelity PR and Development builds. ### Windows 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) + 1. Click the "add python to path" checkbox on the python installer 1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ 1. Open a new command prompt and run From c168d31e07fc4791ecd1230014a74bef987b16d2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 14:52:59 -0800 Subject: [PATCH 110/130] Added missing `"`. --- tools/nitpick/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 2812bc71f5..042a933484 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -35,7 +35,7 @@ Nitpick has 5 functions, separated into separate tabs: ### Mac 1. (first time) Install brew In a terminal: - `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` + `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 1. (First time) install Qt: In a terminal: `brew install qt` From cf831249680b270591558bf62945602d6839776c Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 14:57:25 -0800 Subject: [PATCH 111/130] Improved instructions. --- tools/nitpick/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 042a933484..c5e3a5e21d 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -35,7 +35,8 @@ Nitpick has 5 functions, separated into separate tabs: ### Mac 1. (first time) Install brew In a terminal: - `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` + `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` + Note that you will need to press RETURN again, and will then be asked for your password. 1. (First time) install Qt: In a terminal: `brew install qt` From d71fb2a10085bf760a60b7983526f08ee55877f1 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 7 Feb 2019 14:56:15 -0800 Subject: [PATCH 112/130] moved missed QtWebEngine references to +webengines --- .../resources/qml/+webengine/QmlWebWindow.qml | 108 +++++ interface/resources/qml/QmlWebWindow.qml | 43 -- .../qml/controlsUit/+webengine/WebSpinner.qml | 24 + .../resources/qml/controlsUit/WebSpinner.qml | 8 +- .../qml/hifi/+webengine/DesktopWebEngine.qml | 44 ++ interface/resources/qml/hifi/Desktop.qml | 42 +- .../resources/qml/hifi/DesktopWebEngine.qml | 17 + interface/resources/qml/hifi/WebBrowser.qml | 443 ------------------ .../resources/qml/hifi/avatarapp/Spinner.qml | 1 - .../commerce/marketplace/MarketplaceItem.qml | 1 - .../qml/hifi/tablet/BlocksWebView.qml | 2 - .../resources/qml/hifi/tablet/TabletMenu.qml | 2 - .../qml/hifi/tablet/TabletWebView.qml | 2 - .../qml/hifi/tablet/WindowWebView.qml | 2 - 14 files changed, 208 insertions(+), 531 deletions(-) create mode 100644 interface/resources/qml/+webengine/QmlWebWindow.qml create mode 100644 interface/resources/qml/controlsUit/+webengine/WebSpinner.qml create mode 100644 interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml create mode 100644 interface/resources/qml/hifi/DesktopWebEngine.qml delete mode 100644 interface/resources/qml/hifi/WebBrowser.qml diff --git a/interface/resources/qml/+webengine/QmlWebWindow.qml b/interface/resources/qml/+webengine/QmlWebWindow.qml new file mode 100644 index 0000000000..2e3718f6f5 --- /dev/null +++ b/interface/resources/qml/+webengine/QmlWebWindow.qml @@ -0,0 +1,108 @@ +// +// QmlWebWindow.qml +// +// Created by Bradley Austin Davis on 17 Dec 2015 +// 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 +// + +import QtQuick 2.5 +import QtWebEngine 1.1 +import QtWebChannel 1.0 + +import "qrc:////qml//windows" as Windows +import controlsUit 1.0 as Controls +import stylesUit 1.0 + +Windows.ScrollingWindow { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + shown: false + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false + property alias source: webview.url + property alias scriptUrl: webview.userScriptUrl + + // This is for JS/QML communication, which is unused in a WebWindow, + // but not having this here results in spurious warnings about a + // missing signal + signal sendToScript(var message); + + signal moved(vector2d position); + signal resized(size size); + + function notifyMoved() { + moved(Qt.vector2d(x, y)); + } + + function notifyResized() { + resized(Qt.size(width, height)); + } + + onXChanged: notifyMoved(); + onYChanged: notifyMoved(); + + onWidthChanged: notifyResized(); + onHeightChanged: notifyResized(); + + onShownChanged: { + keyboardEnabled = HMD.active; + } + + Item { + width: pane.contentWidth + implicitHeight: pane.scrollHeight + + Controls.WebView { + id: webview + url: "about:blank" + anchors.fill: parent + focus: true + profile: HFWebEngineProfile; + + property string userScriptUrl: "" + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webview.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + function onWebEventReceived(event) { + if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + } + } + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + eventBridge.webEventReceived.connect(onWebEventReceived); + } + } + } +} diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 322535641d..5da10906ff 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -9,8 +9,6 @@ // import QtQuick 2.5 -import QtWebEngine 1.1 -import QtWebChannel 1.0 import "windows" as Windows import controlsUit 1.0 as Controls @@ -62,47 +60,6 @@ Windows.ScrollingWindow { url: "about:blank" anchors.fill: parent focus: true - profile: HFWebEngineProfile; - - property string userScriptUrl: "" - - // Create a global EventBridge object for raiseAndLowerKeyboard. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation - worldId: WebEngineScript.MainWorld - } - - // Detect when may want to raise and lower keyboard. - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: webview.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] - - function onWebEventReceived(event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); - } - } - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - eventBridge.webEventReceived.connect(onWebEventReceived); - } } } } diff --git a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml new file mode 100644 index 0000000000..e8e01c4865 --- /dev/null +++ b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml @@ -0,0 +1,24 @@ +// +// WebSpinner.qml +// +// Created by David Rowe on 23 May 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtWebEngine 1.5 + +AnimatedImage { + property WebEngineView webview: parent + source: "../../icons/loader-snake-64-w.gif" + visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) + playing: visible + z: 10000 + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } +} diff --git a/interface/resources/qml/controlsUit/WebSpinner.qml b/interface/resources/qml/controlsUit/WebSpinner.qml index e8e01c4865..fb3dc3a8ab 100644 --- a/interface/resources/qml/controlsUit/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/WebSpinner.qml @@ -9,10 +9,14 @@ // import QtQuick 2.5 -import QtWebEngine 1.5 AnimatedImage { - property WebEngineView webview: parent + Item { + id: webView + property bool loading: false + property string url: "" + } + source: "../../icons/loader-snake-64-w.gif" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible diff --git a/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml b/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml new file mode 100644 index 0000000000..56cc38254f --- /dev/null +++ b/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml @@ -0,0 +1,44 @@ +import QtQuick 2.7 +import QtWebEngine 1.5 + +Item { + id: root + + property bool webViewProfileSetup: false + property string currentUrl: "" + property string downloadUrl: "" + property string adaptedPath: "" + property string tempDir: "" + function setupWebEngineSettings() { + WebEngine.settings.javascriptCanOpenWindows = true; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = false; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + } + + + function initWebviewProfileHandlers(profile) { + downloadUrl = currentUrl; + if (webViewProfileSetup) return; + webViewProfileSetup = true; + + profile.downloadRequested.connect(function(download){ + adaptedPath = File.convertUrlToPath(downloadUrl); + tempDir = File.getTempDir(); + download.path = tempDir + "/" + adaptedPath; + download.accept(); + if (download.state === WebEngineDownloadItem.DownloadInterrupted) { + console.log("download failed to complete"); + } + }) + + profile.downloadFinished.connect(function(download){ + if (download.state === WebEngineDownloadItem.DownloadCompleted) { + File.runUnzip(download.path, downloadUrl, autoAdd); + } else { + console.log("The download was corrupted, state: " + download.state); + } + autoAdd = false; + }) + } +} diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 731477e2ae..c44ebdbab1 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,5 +1,4 @@ import QtQuick 2.7 -import QtWebEngine 1.5; import Qt.labs.settings 1.0 as QtSettings import QtQuick.Controls 2.3 @@ -88,43 +87,20 @@ OriginalDesktop.Desktop { })({}); Component.onCompleted: { - WebEngine.settings.javascriptCanOpenWindows = true; - WebEngine.settings.javascriptCanAccessClipboard = false; - WebEngine.settings.spatialNavigationEnabled = false; - WebEngine.settings.localContentCanAccessRemoteUrls = true; + webEngineConfig.setupWebEngineSettings(); } // Accept a download through the webview - property bool webViewProfileSetup: false - property string currentUrl: "" - property string downloadUrl: "" - property string adaptedPath: "" - property string tempDir: "" + property alias webViewProfileSetup: webEngineConfig.webViewProfileSetup + property alias currentUrl: webEngineConfig.currentUrl + property alias downloadUrl: webEngineConfig.downloadUrl + property alias adaptedPath: webEngineConfig.adaptedPath + property alias tempDir: webEngineConfig.tempDir + property var initWebviewProfileHandlers: webEngineConfig.initWebviewProfileHandlers property bool autoAdd: false - function initWebviewProfileHandlers(profile) { - downloadUrl = currentUrl; - if (webViewProfileSetup) return; - webViewProfileSetup = true; - - profile.downloadRequested.connect(function(download){ - adaptedPath = File.convertUrlToPath(downloadUrl); - tempDir = File.getTempDir(); - download.path = tempDir + "/" + adaptedPath; - download.accept(); - if (download.state === WebEngineDownloadItem.DownloadInterrupted) { - console.log("download failed to complete"); - } - }) - - profile.downloadFinished.connect(function(download){ - if (download.state === WebEngineDownloadItem.DownloadCompleted) { - File.runUnzip(download.path, downloadUrl, autoAdd); - } else { - console.log("The download was corrupted, state: " + download.state); - } - autoAdd = false; - }) + DesktopWebEngine { + id: webEngineConfig } function setAutoAdd(auto) { diff --git a/interface/resources/qml/hifi/DesktopWebEngine.qml b/interface/resources/qml/hifi/DesktopWebEngine.qml new file mode 100644 index 0000000000..58c6244e7e --- /dev/null +++ b/interface/resources/qml/hifi/DesktopWebEngine.qml @@ -0,0 +1,17 @@ +import QtQuick 2.7 + +Item { + id: root + + property bool webViewProfileSetup: false + property string currentUrl: "" + property string downloadUrl: "" + property string adaptedPath: "" + property string tempDir: "" + function setupWebEngineSettings() { + } + + + function initWebviewProfileHandlers(profile) { + } +} diff --git a/interface/resources/qml/hifi/WebBrowser.qml b/interface/resources/qml/hifi/WebBrowser.qml deleted file mode 100644 index c05de26471..0000000000 --- a/interface/resources/qml/hifi/WebBrowser.qml +++ /dev/null @@ -1,443 +0,0 @@ - -// -// WebBrowser.qml -// -// -// Created by Vlad Stelmahovsky on 06/22/2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 -import QtQuick.Controls 2.2 as QQControls -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 - -import QtWebEngine 1.5 -import QtWebChannel 1.0 - -import stylesUit 1.0 -import controlsUit 1.0 as HifiControls -import "../windows" -import "../controls" - -import HifiWeb 1.0 - -Rectangle { - id: root; - - HifiConstants { id: hifi; } - - property string title: ""; - signal sendToScript(var message); - property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false - property bool keyboardRaised: false - property bool punctuationMode: false - property var suggestionsList: [] - readonly property string searchUrlTemplate: "https://www.google.com/search?client=hifibrowser&q="; - - - WebBrowserSuggestionsEngine { - id: searchEngine - - onSuggestions: { - if (suggestions.length > 0) { - suggestionsList = [] - suggestionsList.push(addressBarInput.text); //do not overwrite edit text - for(var i = 0; i < suggestions.length; i++) { - suggestionsList.push(suggestions[i]); - } - addressBar.model = suggestionsList - if (!addressBar.popup.visible) { - addressBar.popup.open(); - } - } - } - } - - Timer { - id: suggestionRequestTimer - interval: 200 - repeat: false - onTriggered: { - if (addressBar.editText !== "") { - searchEngine.querySuggestions(addressBarInput.text); - } - } - } - - color: hifi.colors.baseGray; - - function goTo(url) { - //must be valid attempt to open an site with dot - var urlNew = url - if (url.indexOf(".") > 0) { - if (url.indexOf("http") < 0) { - urlNew = "http://" + url; - } - } else { - urlNew = searchUrlTemplate + url - } - - addressBar.model = [] - //need to rebind if binfing was broken by selecting from suggestions - addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; }); - webStack.currentItem.webEngineView.url = urlNew - suggestionRequestTimer.stop(); - addressBar.popup.close(); - } - - Column { - spacing: 2 - width: parent.width; - - RowLayout { - id: addressBarRow - width: parent.width; - height: 48 - - HifiControls.WebGlyphButton { - enabled: webStack.currentItem.webEngineView.canGoBack || webStack.depth > 1 - glyph: hifi.glyphs.backward; - anchors.verticalCenter: parent.verticalCenter; - size: 38; - onClicked: { - if (webStack.currentItem.webEngineView.canGoBack) { - webStack.currentItem.webEngineView.goBack(); - } else if (webStack.depth > 1) { - webStack.pop(); - } - } - } - - HifiControls.WebGlyphButton { - enabled: webStack.currentItem.webEngineView.canGoForward - glyph: hifi.glyphs.forward; - anchors.verticalCenter: parent.verticalCenter; - size: 38; - onClicked: { - webStack.currentItem.webEngineView.goForward(); - } - } - - QQControls.ComboBox { - id: addressBar - - //selectByMouse: true - focus: true - - editable: true - //flat: true - indicator: Item {} - background: Item {} - onActivated: { - goTo(textAt(index)); - } - - onHighlightedIndexChanged: { - if (highlightedIndex >= 0) { - addressBar.editText = textAt(highlightedIndex) - } - } - - popup.height: webStack.height - - onFocusChanged: { - if (focus) { - addressBarInput.selectAll(); - } - } - - contentItem: QQControls.TextField { - id: addressBarInput - leftPadding: 26 - rightPadding: hifi.dimensions.controlLineHeight + 5 - text: addressBar.editText - placeholderText: qsTr("Enter URL") - font: addressBar.font - selectByMouse: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - onFocusChanged: { - if (focus) { - selectAll(); - } - } - - Keys.onDeletePressed: { - addressBarInput.text = "" - } - - Keys.onPressed: { - if (event.key === Qt.Key_Return) { - goTo(addressBarInput.text); - event.accepted = true; - } - } - - Image { - anchors.verticalCenter: parent.verticalCenter; - x: 5 - z: 2 - id: faviconImage - width: 16; height: 16 - sourceSize: Qt.size(width, height) - source: webStack.currentItem.webEngineView.icon - } - - HifiControls.WebGlyphButton { - glyph: webStack.currentItem.webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall; - anchors.verticalCenter: parent.verticalCenter; - width: hifi.dimensions.controlLineHeight - z: 2 - x: addressBarInput.width - implicitWidth - onClicked: { - if (webStack.currentItem.webEngineView.loading) { - webStack.currentItem.webEngineView.stop(); - } else { - webStack.currentItem.reloadTimer.start(); - } - } - } - } - - Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i"); - - Keys.onPressed: { - if (event.key === Qt.Key_Return) { - goTo(addressBarInput.text); - event.accepted = true; - } - } - - onEditTextChanged: { - if (addressBar.editText !== "" && addressBar.editText !== webStack.currentItem.webEngineView.url.toString()) { - suggestionRequestTimer.restart(); - } else { - addressBar.model = [] - addressBar.popup.close(); - } - - } - - Layout.fillWidth: true - editText: webStack.currentItem.webEngineView.url - onAccepted: goTo(addressBarInput.text); - } - - HifiControls.WebGlyphButton { - checkable: true - checked: webStack.currentItem.webEngineView.audioMuted - glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted - anchors.verticalCenter: parent.verticalCenter; - width: hifi.dimensions.controlLineHeight - onClicked: { - webStack.currentItem.webEngineView.audioMuted = !webStack.currentItem.webEngineView.audioMuted - } - } - } - - QQControls.ProgressBar { - id: loadProgressBar - background: Rectangle { - implicitHeight: 2 - color: "#6A6A6A" - } - - contentItem: Item { - implicitHeight: 2 - - Rectangle { - width: loadProgressBar.visualPosition * parent.width - height: parent.height - color: "#00B4EF" - } - } - - width: parent.width; - from: 0 - to: 100 - value: webStack.currentItem.webEngineView.loadProgress - height: 2 - } - - Component { - id: webViewComponent - Rectangle { - property alias webEngineView: webEngineView - property alias reloadTimer: reloadTimer - - property WebEngineNewViewRequest request: null - - property bool isDialog: QQControls.StackView.index > 0 - property real margins: isDialog ? 10 : 0 - - color: "#d1d1d1" - - QQControls.StackView.onActivated: { - addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; }); - } - - onRequestChanged: { - if (isDialog && request !== null && request !== undefined) {//is Dialog ? - request.openIn(webEngineView); - } - } - - HifiControls.BaseWebView { - id: webEngineView - anchors.fill: parent - anchors.margins: parent.margins - - layer.enabled: parent.isDialog - layer.effect: DropShadow { - verticalOffset: 8 - horizontalOffset: 8 - color: "#330066ff" - samples: 10 - spread: 0.5 - } - - focus: true - objectName: "tabletWebEngineView" - - //profile: HFWebEngineProfile; - profile.httpUserAgent: "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0" - - property string userScriptUrl: "" - - onLoadingChanged: { - if (!loading) { - addressBarInput.cursorPosition = 0 //set input field cursot to beginning - suggestionRequestTimer.stop(); - addressBar.popup.close(); - } - } - - onLinkHovered: { - //TODO: change cursor shape? - } - - // creates a global EventBridge object. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.Deferred - worldId: WebEngineScript.MainWorld - } - - // detects when to raise and lower virtual keyboard - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: webEngineView.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] - - settings.autoLoadImages: true - settings.javascriptEnabled: true - settings.errorPageEnabled: true - settings.pluginsEnabled: true - settings.fullScreenSupportEnabled: true - settings.autoLoadIconsForPage: true - settings.touchIconsEnabled: true - - onCertificateError: { - error.defer(); - } - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - } - - onFeaturePermissionRequested: { - grantFeaturePermission(securityOrigin, feature, true); - } - - onNewViewRequested: { - if (request.destination == WebEngineView.NewViewInDialog) { - webStack.push(webViewComponent, {"request": request}); - } else { - request.openIn(webEngineView); - } - } - - onRenderProcessTerminated: { - var status = ""; - switch (terminationStatus) { - case WebEngineView.NormalTerminationStatus: - status = "(normal exit)"; - break; - case WebEngineView.AbnormalTerminationStatus: - status = "(abnormal exit)"; - break; - case WebEngineView.CrashedTerminationStatus: - status = "(crashed)"; - break; - case WebEngineView.KilledTerminationStatus: - status = "(killed)"; - break; - } - - console.error("Render process exited with code " + exitCode + " " + status); - reloadTimer.running = true; - } - - onFullScreenRequested: { - if (request.toggleOn) { - webEngineView.state = "FullScreen"; - } else { - webEngineView.state = ""; - } - request.accept(); - } - - onWindowCloseRequested: { - webStack.pop(); - } - } - Timer { - id: reloadTimer - interval: 0 - running: false - repeat: false - onTriggered: webEngineView.reload() - } - } - } - - QQControls.StackView { - id: webStack - width: parent.width; - property real webViewHeight: root.height - loadProgressBar.height - 48 - 4 - height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight - - Component.onCompleted: webStack.push(webViewComponent, {"webEngineView.url": "https://www.highfidelity.com"}); - } - } - - - HifiControls.Keyboard { - id: keyboard - raised: parent.keyboardEnabled && parent.keyboardRaised - numeric: parent.punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - } -} diff --git a/interface/resources/qml/hifi/avatarapp/Spinner.qml b/interface/resources/qml/hifi/avatarapp/Spinner.qml index 3fc331346d..14f8e922d7 100644 --- a/interface/resources/qml/hifi/avatarapp/Spinner.qml +++ b/interface/resources/qml/hifi/avatarapp/Spinner.qml @@ -1,5 +1,4 @@ import QtQuick 2.5 -import QtWebEngine 1.5 AnimatedImage { source: "../../../icons/loader-snake-64-w.gif" diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 24ef528673..1909157a79 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -15,7 +15,6 @@ import Hifi 1.0 as Hifi import QtQuick 2.9 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import QtWebEngine 1.5 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls diff --git a/interface/resources/qml/hifi/tablet/BlocksWebView.qml b/interface/resources/qml/hifi/tablet/BlocksWebView.qml index 1e9eb3beb4..03fce0a112 100644 --- a/interface/resources/qml/hifi/tablet/BlocksWebView.qml +++ b/interface/resources/qml/hifi/tablet/BlocksWebView.qml @@ -1,6 +1,4 @@ import QtQuick 2.0 -import QtWebEngine 1.2 - import "../../controls" as Controls Controls.TabletWebView { diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index 267fb9f0cf..5f06e4fbab 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -2,8 +2,6 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import QtQml 2.2 -import QtWebChannel 1.0 -import QtWebEngine 1.1 import "." diff --git a/interface/resources/qml/hifi/tablet/TabletWebView.qml b/interface/resources/qml/hifi/tablet/TabletWebView.qml index ff6be0480f..9eba7824e0 100644 --- a/interface/resources/qml/hifi/tablet/TabletWebView.qml +++ b/interface/resources/qml/hifi/tablet/TabletWebView.qml @@ -1,6 +1,4 @@ import QtQuick 2.0 -import QtWebEngine 1.2 - import "../../controls" as Controls Controls.TabletWebScreen { diff --git a/interface/resources/qml/hifi/tablet/WindowWebView.qml b/interface/resources/qml/hifi/tablet/WindowWebView.qml index 0f697d634e..632ab712cb 100644 --- a/interface/resources/qml/hifi/tablet/WindowWebView.qml +++ b/interface/resources/qml/hifi/tablet/WindowWebView.qml @@ -1,6 +1,4 @@ import QtQuick 2.0 -import QtWebEngine 1.2 - import "../../controls" as Controls Controls.WebView { From 2617febbcd8a31de616beb35347568d00d77bedd Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 15:31:13 -0800 Subject: [PATCH 113/130] Replace ad-hoc audio meter with meter calibrated in dBFS --- interface/src/scripting/Audio.cpp | 24 ++++------------------ libraries/audio-client/src/AudioClient.cpp | 2 +- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 9ccb698da0..d9185a9a70 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -15,6 +15,7 @@ #include "Application.h" #include "AudioClient.h" +#include "AudioHelpers.h" #include "ui/AvatarInputs.h" using namespace scripting; @@ -26,26 +27,9 @@ QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; float Audio::loudnessToLevel(float loudness) { - const float LOG2 = log(2.0f); - const float METER_LOUDNESS_SCALE = 2.8f / 5.0f; - const float LOG2_LOUDNESS_FLOOR = 11.0f; - - float level = 0.0f; - - loudness += 1.0f; - float log2loudness = logf(loudness) / LOG2; - - if (log2loudness <= LOG2_LOUDNESS_FLOOR) { - level = (log2loudness / LOG2_LOUDNESS_FLOOR) * METER_LOUDNESS_SCALE; - } else { - level = (log2loudness - (LOG2_LOUDNESS_FLOOR - 1.0f)) * METER_LOUDNESS_SCALE; - } - - if (level > 1.0f) { - level = 1.0; - } - - return level; + float level = 6.02059991f * fastLog2f(loudness); // level in dBFS + level = (level + 60.0f) * (1/48.0f); // map [-60, -12] dBFS to [0, 1] + return glm::clamp(level, 0.0f, 1.0f); } Audio::Audio() : _devices(_contextIsHMD) { diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c9cf1646ec..4489d19806 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -175,7 +175,7 @@ static float computeLoudness(int16_t* samples, int numSamples, int numChannels, const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS const int32_t CLIPPING_DETECTION = 3; // consecutive samples over threshold - float scale = numSamples ? 1.0f / numSamples : 0.0f; + float scale = numSamples ? 1.0f / (numSamples * 32768.0f) : 0.0f; int32_t loudness = 0; isClipping = false; From 4bbb030ad74e76f00b0035a04e47c210a6f368ec Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 7 Feb 2019 15:34:44 -0800 Subject: [PATCH 114/130] Show only free items when running from the oculus store --- .../qml/hifi/commerce/marketplace/Marketplace.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 351c43286c..0d42cb599e 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -38,8 +38,8 @@ Rectangle { property bool keyboardEnabled: HMD.active property bool keyboardRaised: false property string searchScopeString: "Featured" - property bool isLoggedIn: false; - property bool supports3DHTML: true; + property bool isLoggedIn: false + property bool supports3DHTML: true anchors.fill: (typeof parent === undefined) ? undefined : parent @@ -49,7 +49,7 @@ Rectangle { licenseInfo.visible = false; marketBrowseModel.getFirstPage(); { - if(root.searchString !== undefined && root.searchString !== "") { + if(root.searchString !== undefined && root.searchString !== "") { root.searchScopeString = "Search Results: \"" + root.searchString + "\""; } else if (root.categoryString !== "") { root.searchScopeString = root.categoryString; @@ -498,7 +498,7 @@ Rectangle { "", "", root.sortString, - false, + WalletScriptingInterface.limitedCommerce, marketBrowseModel.currentPageToRetrieve, marketBrowseModel.itemsPerPage ); From d014dc2d1447d08c0af322f9adb5e1b5450b8cac Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 15:35:20 -0800 Subject: [PATCH 115/130] Add audio meter ballistics for less display jitter (10ms attack, 300ms release) --- libraries/audio-client/src/AudioClient.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4489d19806..60a95ff58a 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1222,7 +1222,11 @@ void AudioClient::handleMicAudioInput() { // detect loudness and clipping on the raw input bool isClipping = false; - _lastInputLoudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); + float inputLoudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); + + float tc = (inputLoudness > _lastInputLoudness) ? 0.378f : 0.967f; // 10ms attack, 300ms release @ 100Hz + inputLoudness += tc * (_lastInputLoudness - inputLoudness); + _lastInputLoudness = inputLoudness; if (isClipping) { _timeSinceLastClip = 0.0f; From 52aa20b4d58074a18033deaae28c8a317d2ea557 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 15:47:56 -0800 Subject: [PATCH 116/130] Add "gated" indicator to audio level meter --- interface/resources/qml/hifi/audio/MicBar.qml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index fee37ca1c1..3dbf55818e 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -16,7 +16,13 @@ import TabletScriptingInterface 1.0 Rectangle { readonly property var level: AudioScriptingInterface.inputLevel; - + + property bool gated: false; + Component.onCompleted: { + AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); + AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + } + property bool standalone: false; property var dragTarget: null; @@ -189,7 +195,7 @@ Rectangle { Rectangle { // mask id: mask; - width: parent.width * level; + width: gated ? 0 : parent.width * level; radius: 5; anchors { bottom: parent.bottom; @@ -225,5 +231,19 @@ Rectangle { } } } + + Rectangle { + id: gatedIndicator; + visible: gated + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: "#0080FF"; + anchors { + right: parent.left; + verticalCenter: parent.verticalCenter; + } + } } } From d2abf1a56d5e9e67cfa9a064d415f8564b8381d4 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 15:52:29 -0800 Subject: [PATCH 117/130] Add "clipping" indicator to audio level meter --- interface/resources/qml/hifi/audio/MicBar.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 3dbf55818e..615235470f 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -234,7 +234,7 @@ Rectangle { Rectangle { id: gatedIndicator; - visible: gated + visible: gated && !AudioScriptingInterface.clipping radius: 4; width: 2 * radius; @@ -245,5 +245,19 @@ Rectangle { verticalCenter: parent.verticalCenter; } } + + Rectangle { + id: clippingIndicator; + visible: AudioScriptingInterface.clipping + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: colors.red; + anchors { + left: parent.right; + verticalCenter: parent.verticalCenter; + } + } } } From b04a59af1ba7cd4be7e7842d3149dd1f782fc39c Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 16:31:25 -0800 Subject: [PATCH 118/130] Calibrate meter to encourage hotter mic levels --- interface/src/scripting/Audio.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index d9185a9a70..fb64dbe098 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -28,7 +28,7 @@ Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, float Audio::loudnessToLevel(float loudness) { float level = 6.02059991f * fastLog2f(loudness); // level in dBFS - level = (level + 60.0f) * (1/48.0f); // map [-60, -12] dBFS to [0, 1] + level = (level + 48.0f) * (1/39.0f); // map [-48, -9] dBFS to [0, 1] return glm::clamp(level, 0.0f, 1.0f); } From 5681a996a1b9f74810c4c6261d15fe5a2162aafb Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 7 Feb 2019 16:33:41 -0800 Subject: [PATCH 119/130] Fix meter color gradient, to encourage proper mic levels. High levels are now yellow, actual digital clipping sets red indicator light. --- interface/resources/qml/hifi/audio/MicBar.qml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 615235470f..39f75a9182 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -83,6 +83,7 @@ Rectangle { readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; @@ -218,16 +219,12 @@ Rectangle { color: colors.greenStart; } GradientStop { - position: 0.8; + position: 0.5; color: colors.greenEnd; } - GradientStop { - position: 0.81; - color: colors.red; - } GradientStop { position: 1; - color: colors.red; + color: colors.yellow; } } } From 79bf26ee27332aa8a94af93b9fc2ff39a9be423d Mon Sep 17 00:00:00 2001 From: Matt Hardcastle Date: Thu, 7 Feb 2019 15:57:14 -0800 Subject: [PATCH 120/130] Add prebuild metrics logging for CI system Metric about the times various parts of the build take are great to have. This change takes us a step in that direction by adding metrics to the prebuild step of the build process. These metrics are off by default; use the `--ci-build` option of `prebuild.py` to enable them. --- prebuild.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/prebuild.py b/prebuild.py index fb54b8d6fe..060e1fd3b0 100644 --- a/prebuild.py +++ b/prebuild.py @@ -35,9 +35,50 @@ import re import tempfile import time import functools +import subprocess +import logging + +from uuid import uuid4 +from contextlib import contextmanager print = functools.partial(print, flush=True) +class TrackableLogger(logging.Logger): + guid = str(uuid4()) + + def _log(self, msg, *args, **kwargs): + x = {'guid': self.guid} + if 'extra' in kwargs: + kwargs['extra'].update(x) + else: + kwargs['extra'] = x + super()._log(msg, *args, **kwargs) + +logging.setLoggerClass(TrackableLogger) +logger = logging.getLogger('prebuild') + +def headSha(): + repo_dir = os.path.dirname(os.path.abspath(__file__)) + git = subprocess.Popen( + 'git rev-parse --short HEAD', + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True, cwd=repo_dir, universal_newlines=True, + ) + stdout, _ = git.communicate() + sha = stdout.split('\n')[0] + if not sha: + raise RuntimeError("couldn't find git sha") + return sha + +@contextmanager +def timer(name): + ''' Print the elapsed time a context's execution takes to execute ''' + start = time.time() + yield + # Please take care when modifiying this print statement. + # Log parsing logic may depend on it. + logger.info('%s took %.3f secs' % (name, time.time() - start)) + def parse_args(): # our custom ports, relative to the script location defaultPortsPath = hifi_utils.scriptRelative('cmake', 'ports') @@ -50,6 +91,7 @@ def parse_args(): parser.add_argument('--vcpkg-root', type=str, help='The location of the vcpkg distribution') parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build') parser.add_argument('--ports-path', type=str, default=defaultPortsPath) + parser.add_argument('--ci-build', action='store_true') if True: args = parser.parse_args() else: @@ -66,11 +108,19 @@ def main(): del os.environ[var] args = parse_args() + + if args.ci_build: + logging.basicConfig(datefmt='%s', format='%(asctime)s %(guid)s %(message)s', level=logging.INFO) + + logger.info('sha=%s' % headSha()) + logger.info('start') + # Only allow one instance of the program to run at a time pm = hifi_vcpkg.VcpkgRepo(args) with hifi_singleton.Singleton(pm.lockFile) as lock: - if not pm.upToDate(): - pm.bootstrap() + with timer('Bootstraping'): + if not pm.upToDate(): + pm.bootstrap() # Always write the tag, even if we changed nothing. This # allows vcpkg to reclaim disk space by identifying directories with @@ -80,11 +130,13 @@ def main(): # Grab our required dependencies: # * build host tools, like spirv-cross and scribe # * build client dependencies like openssl and nvtt - pm.setupDependencies() + with timer('Setting up dependencies'): + pm.setupDependencies() # wipe out the build directories (after writing the tag, since failure # here shouldn't invalidte the vcpkg install) - pm.cleanBuilds() + with timer('Cleaning builds'): + pm.cleanBuilds() # If we're running in android mode, we also need to grab a bunch of additional binaries # (this logic is all migrated from the old setupDependencies tasks in gradle) @@ -98,7 +150,10 @@ def main(): hifi_android.QtPackager(appPath, qtPath).bundle() # Write the vcpkg config to the build directory last - pm.writeConfig() + with timer('Writing configuration'): + pm.writeConfig() + + logger.info('end') print(sys.argv) main() From 80392b1724d6677786bd1ff4e509103f010b1c81 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 7 Feb 2019 18:21:24 -0800 Subject: [PATCH 121/130] Removed unneeded copying of SSL DLL's. --- tools/nitpick/CMakeLists.txt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index f825775879..e69b16b866 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -112,8 +112,8 @@ foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) # perform the system include hack for OS X to ignore warnings if (APPLE) foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") - endforeach() + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") + endforeach() endif () if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) @@ -148,7 +148,7 @@ if (WIN32) POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" ) - + if (RELEASE_TYPE STREQUAL "DEV") # This to enable running from the IDE add_custom_command( @@ -157,13 +157,6 @@ if (WIN32) COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity" ) endif () - - # add a custom command to copy the SSL DLLs - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" - ) elseif (APPLE) add_custom_command( TARGET ${TARGET_NAME} From e6b2c890d0ec2354bddd282f290547cd005425d3 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 7 Feb 2019 22:21:05 -0800 Subject: [PATCH 122/130] Case21085 - Changing domains with scripting APIs not working AccountManager was earlier changed to support QUrlQuery for query strings in it's 'send.' Unfortunately, QUrlQuery isn't a scriptable property type so using AM was failing from scripts --- interface/src/commerce/QmlMarketplace.cpp | 14 +++++-------- interface/src/commerce/QmlMarketplace.h | 7 ++++++- libraries/networking/src/AccountManager.cpp | 23 ++++++++++++--------- libraries/networking/src/AccountManager.h | 5 ++--- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp index 07a9e570bd..23ba418a2d 100644 --- a/interface/src/commerce/QmlMarketplace.cpp +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -72,20 +72,17 @@ void QmlMarketplace::getMarketplaceItems( void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) { QString endpoint = QString("items/") + marketplaceItemId; - QUrlQuery request; - send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); + send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional); } void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) { QString endpoint = QString("items/") + marketplaceItemId + "/like"; - QUrlQuery request; - send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); + send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required); } void QmlMarketplace::getMarketplaceCategories() { QString endpoint = "categories"; - QUrlQuery request; - send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request); + send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None); } @@ -94,14 +91,13 @@ void QmlMarketplace::send(const QString& endpoint, const QString& success, const const QString URL = "/api/v1/marketplace/"; JSONCallbackParameters callbackParams(this, success, fail); - accountManager->sendRequest(URL + endpoint, + accountManager->sendRequest(URL + endpoint + "?" + request.toString(), authType, method, callbackParams, QByteArray(), NULL, - QVariantMap(), - request); + QVariantMap()); } diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h index f954198371..5794d4f53c 100644 --- a/interface/src/commerce/QmlMarketplace.h +++ b/interface/src/commerce/QmlMarketplace.h @@ -60,7 +60,12 @@ signals: void marketplaceItemLikeResult(QJsonObject result); private: - void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request); + void send(const QString& endpoint, + const QString& success, + const QString& fail, + QNetworkAccessManager::Operation method, + AccountManagerAuth::Type authType, + const QUrlQuery& request = QUrlQuery()); QJsonObject apiResponse(const QString& label, QNetworkReply* reply); QJsonObject failResponse(const QString& label, QNetworkReply* reply); }; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index cf77c1cad5..4647c50496 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -208,7 +208,7 @@ void AccountManager::setSessionID(const QUuid& sessionID) { } } -QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query) { +QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); @@ -217,17 +217,22 @@ QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth:: uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); QUrl requestURL = _authURL; - + if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL. requestURL = getMetaverseServerURL(); } + int queryStringLocation = path.indexOf("?"); if (path.startsWith("/")) { - requestURL.setPath(path); + requestURL.setPath(path.left(queryStringLocation)); } else { - requestURL.setPath("/" + path); + requestURL.setPath("/" + path.left(queryStringLocation)); + } + + if (queryStringLocation >= 0) { + QUrlQuery query(path.mid(queryStringLocation+1)); + requestURL.setQuery(query); } - requestURL.setQuery(query); if (authType != AccountManagerAuth::None ) { if (hasValidAccessToken()) { @@ -253,8 +258,7 @@ void AccountManager::sendRequest(const QString& path, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart, - const QVariantMap& propertyMap, - QUrlQuery query) { + const QVariantMap& propertyMap) { if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "sendRequest", @@ -264,14 +268,13 @@ void AccountManager::sendRequest(const QString& path, Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart), - Q_ARG(QVariantMap, propertyMap), - Q_ARG(QUrlQuery, query)); + Q_ARG(QVariantMap, propertyMap)); return; } QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = createRequest(path, authType, query); + QNetworkRequest networkRequest = createRequest(path, authType); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString()); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 2ccebdd73c..8732042e93 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -61,15 +61,14 @@ class AccountManager : public QObject, public Dependency { public: AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); - QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query = QUrlQuery()); + QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL, - const QVariantMap& propertyMap = QVariantMap(), - QUrlQuery query = QUrlQuery()); + const QVariantMap& propertyMap = QVariantMap()); void setIsAgent(bool isAgent) { _isAgent = isAgent; } From 5b7d7b88331e0d5607cb6676b52b5056d7846cca Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Fri, 8 Feb 2019 08:53:43 -0800 Subject: [PATCH 123/130] Pack all non-instanced traits Previously this code only would pack the skeletonModelURL trait. Which is technically not a bug, because there it is the only non-instanced trait. But, we plan to add new traits in the future. So, lets fix this now. --- libraries/avatars/src/ClientTraitsHandler.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..3e188afbdf 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -107,11 +107,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { if (initialSend || *simpleIt == Updated) { if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); - // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); } ++simpleIt; From 76a56c679a3c95209b7343bc239e97586e165230 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 8 Feb 2019 10:03:05 -0800 Subject: [PATCH 124/130] making requested changes --- interface/resources/images/unsupportedImage.png | Bin 0 -> 24855 bytes interface/resources/qml/QmlWebWindow.qml | 1 + .../qml/controlsUit/+webengine/WebSpinner.qml | 2 +- .../resources/qml/controlsUit/WebSpinner.qml | 4 ++-- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 interface/resources/images/unsupportedImage.png diff --git a/interface/resources/images/unsupportedImage.png b/interface/resources/images/unsupportedImage.png new file mode 100644 index 0000000000000000000000000000000000000000..87d238b67c9c66ec715d994269bda796b929ebab GIT binary patch literal 24855 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 QZ2$lO07*qoM6N<$g7ykz9RL6T literal 0 HcmV?d00001 diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 5da10906ff..a40168039e 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -58,6 +58,7 @@ Windows.ScrollingWindow { Controls.WebView { id: webview url: "about:blank" + property string userScriptUrl: "" anchors.fill: parent focus: true } diff --git a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml index e8e01c4865..c2e2ca4b40 100644 --- a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml @@ -13,7 +13,7 @@ import QtWebEngine 1.5 AnimatedImage { property WebEngineView webview: parent - source: "../../icons/loader-snake-64-w.gif" + source: "qrc:////icons//loader-snake-64-w.gif" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible z: 10000 diff --git a/interface/resources/qml/controlsUit/WebSpinner.qml b/interface/resources/qml/controlsUit/WebSpinner.qml index fb3dc3a8ab..bcf415e0c0 100644 --- a/interface/resources/qml/controlsUit/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/WebSpinner.qml @@ -10,14 +10,14 @@ import QtQuick 2.5 -AnimatedImage { +Image { Item { id: webView property bool loading: false property string url: "" } - source: "../../icons/loader-snake-64-w.gif" + source: "qrc:////images//unsupportedImage.png" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible z: 10000 From 4727123e5f95f941b94901a630545cc9bdad7f26 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Feb 2019 10:25:18 -0800 Subject: [PATCH 125/130] Do not include nitpick in production builds. --- tools/CMakeLists.txt | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 886f15ded4..ea9d4b8496 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -19,20 +19,36 @@ function(check_test name) endfunction() if (BUILD_TOOLS) - set(ALL_TOOLS - udt-test - vhacd-util - frame-optimizer - gpu-frame-player - ice-client - ktx-tool - ac-client - skeleton-dump - atp-client - oven - nitpick - ) - + # Allow different tools for production builds + if (RELEASE_TYPE STREQUAL "PRODUCTION") + set(ALL_TOOLS + udt-test + vhacd-util + frame-optimizer + gpu-frame-player + ice-client + ktx-tool + ac-client + skeleton-dump + atp-client + oven + ) + else() + set(ALL_TOOLS + udt-test + vhacd-util + frame-optimizer + gpu-frame-player + ice-client + ktx-tool + ac-client + skeleton-dump + atp-client + oven + ####nitpick + ) + endif() + foreach(TOOL ${ALL_TOOLS}) check_test(${TOOL}) if (${BUILD_TOOL_RESULT}) From 6598f83d39435e5a945c6bd13d3357f9291d1a7b Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Feb 2019 11:22:39 -0800 Subject: [PATCH 126/130] Removed debug code. --- tools/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index ea9d4b8496..4d7a594d92 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -45,7 +45,7 @@ if (BUILD_TOOLS) skeleton-dump atp-client oven - ####nitpick + nitpick ) endif() From dff98f462f621493fa31c61df033cdb13b3fe79c Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 9 Feb 2019 13:13:13 -0800 Subject: [PATCH 127/130] Removed unnecessary of spaces. --- tools/nitpick/ui/Nitpick.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 319452233f..16aaa9594d 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -43,7 +43,7 @@ - 3 + 0 @@ -85,7 +85,7 @@ - Create all MD files + Create all MD files @@ -124,7 +124,7 @@ - Create all Recursive Scripts + Create all Recursive Scripts From b2fb7a737be139b4a53f586d6e2550b8187c03bf Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 9 Feb 2019 13:21:36 -0800 Subject: [PATCH 128/130] Ready for testing. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/Test.cpp | 154 +++++++++++++--------------------- tools/nitpick/src/Test.h | 9 +- 3 files changed, 65 insertions(+), 100 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 78ed0ca0af..fa53730ce0 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -40,7 +40,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v2.0.1"); + setWindowTitle("Nitpick - v2.1.0"); } Nitpick::~Nitpick() { diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index 2e62296146..59583cda8b 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -758,47 +758,66 @@ void Test::createAllRecursiveScripts() { return; } + createAllRecursiveScripts(_testsRootDirectory); createRecursiveScript(_testsRootDirectory, false); - - QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir; - if (!isAValidDirectory(directory)) { - continue; - } - - // Only process directories that have sub-directories - bool hasNoSubDirectories{ true }; - QDirIterator it2(directory, QDirIterator::Subdirectories); - while (it2.hasNext()) { - QString directory2 = it2.next(); - - // Only process directories - QDir dir; - if (isAValidDirectory(directory2)) { - hasNoSubDirectories = false; - break; - } - } - - if (!hasNoSubDirectories) { - createRecursiveScript(directory, false); - } - } - QMessageBox::information(0, "Success", "Scripts have been created"); } -void Test::createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode) { - const QString recursiveTestsScriptName("testRecursive.js"); - const QString recursiveTestsFilename(topLevelDirectory + "/" + recursiveTestsScriptName); +void Test::createAllRecursiveScripts(const QString& directory) { + QDirIterator it(directory, QDirIterator::Subdirectories); + + while (it.hasNext()) { + QString nextDirectory = it.next(); + if (isAValidDirectory(nextDirectory)) { + createAllRecursiveScripts(nextDirectory); + createRecursiveScript(nextDirectory, false); + } + } +} + +void Test::createRecursiveScript(const QString& directory, bool interactiveMode) { + // If folder contains a test, then we are at a leaf + const QString testPathname{ directory + "/" + TEST_FILENAME }; + if (QFileInfo(testPathname).exists()) { + return; + } + + // Directories are included in reverse order. The nitpick scripts use a stack mechanism, + // so this ensures that the tests run in alphabetical order (a convenience when debugging) + QStringList directories; + QDirIterator it(directory); + while (it.hasNext()) { + QString subDirectory = it.next(); + + // Only process directories + if (!isAValidDirectory(subDirectory)) { + continue; + } + + const QString testPathname{ subDirectory + "/" + TEST_FILENAME }; + if (QFileInfo(testPathname).exists()) { + // Current folder contains a test script + directories.push_front(testPathname); + } + + const QString testRecursivePathname{ subDirectory + "/" + TEST_RECURSIVE_FILENAME }; + if (QFileInfo(testRecursivePathname).exists()) { + // Current folder contains a recursive script + directories.push_front(testRecursivePathname); + } + } + + // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant + if (directories.length() == 0) { + return; + } + + // Open the recursive script file + const QString recursiveTestsFilename(directory + "/" + TEST_RECURSIVE_FILENAME); QFile recursiveTestsFile(recursiveTestsFilename); if (!recursiveTestsFile.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Failed to create \"" + recursiveTestsScriptName + "\" in directory \"" + topLevelDirectory + "\""); + "Failed to create \"" + TEST_RECURSIVE_FILENAME + "\" in directory \"" + directory + "\""); exit(-1); } @@ -812,72 +831,16 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact QString user = nitpick->getSelectedUser(); textStream << "PATH_TO_THE_REPO_PATH_UTILS_FILE = \"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + - "/tests/utils/branchUtils.js\";" - << endl; - textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl; - textStream << "var nitpick = createNitpick(Script.resolvePath(\".\"));" << endl << endl; + "/tests/utils/branchUtils.js\";" + << endl; + textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl << endl; - textStream << "var testsRootPath = nitpick.getTestsRootPath();" << endl << endl; - - // Wait 10 seconds before starting - textStream << "if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << "};" << endl << endl; + textStream << "if (typeof nitpick === 'undefined') var nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; + textStream << "if (typeof testsRootPath === 'undefined') var testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << "nitpick.enableRecursive();" << endl; textStream << "nitpick.enableAuto();" << endl << endl; - // This is used to verify that the recursive test contains at least one test - bool testFound{ false }; - - // Directories are included in reverse order. The nitpick scripts use a stack mechanism, - // so this ensures that the tests run in alphabetical order (a convenience when debugging) - QStringList directories; - - // First test if top-level folder has a test.js file - const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - - QDirIterator it(topLevelDirectory, QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir(directory); - if (!isAValidDirectory(directory)) { - continue; - } - - const QString testPathname{ directory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - } - - if (interactiveMode && !testFound) { - QMessageBox::information(0, "Failure", "No \"" + TEST_FILENAME + "\" files found"); - recursiveTestsFile.close(); - return; - } - - // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant - // The script will be closed and deleted - if (directories.length() == 0) { - recursiveTestsFile.close(); - QFile::remove(recursiveTestsFilename); - return; - } - // Now include the test scripts for (int i = 0; i < directories.length(); ++i) { includeTest(textStream, directories.at(i)); @@ -928,7 +891,6 @@ void Test::createTestsOutline() { QString directory = it.next(); // Only process directories - QDir dir; if (!isAValidDirectory(directory)) { continue; } diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index aafd2f5711..842e4bdb48 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -72,9 +72,11 @@ public: void updateTestRailRunResult(); - void createRecursiveScript(); void createAllRecursiveScripts(); - void createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode); + void createAllRecursiveScripts(const QString& directory); + + void createRecursiveScript(); + void createRecursiveScript(const QString& directory, bool interactiveMode); int compareImageLists(); int checkTextResults(); @@ -109,7 +111,8 @@ private: bool _isRunningFromCommandLine{ false }; bool _isRunningInAutomaticTestRun{ false }; - const QString TEST_FILENAME { "test.js" }; + const QString TEST_FILENAME{ "test.js" }; + const QString TEST_RECURSIVE_FILENAME{ "testRecursive.js" }; const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; From d3e0aa5d8cd2cb69afb1afadadb17b29a9187a5b Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sun, 10 Feb 2019 20:32:38 -0800 Subject: [PATCH 129/130] Make sure only top-level recursive script runs recursively. --- tools/nitpick/src/Test.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index 59583cda8b..20d5ad89a6 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -835,8 +835,8 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode) << endl; textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl << endl; - textStream << "if (typeof nitpick === 'undefined') var nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; - textStream << "if (typeof testsRootPath === 'undefined') var testsRootPath = nitpick.getTestsRootPath();" << endl << endl; + textStream << "if (typeof nitpick === 'undefined') nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; + textStream << "if (typeof testsRootPath === 'undefined') testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << "nitpick.enableRecursive();" << endl; textStream << "nitpick.enableAuto();" << endl << endl; @@ -847,7 +847,10 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode) } textStream << endl; - textStream << "nitpick.runRecursive();" << endl; + textStream << "if (typeof runningRecursive === 'undefined') {" << endl; + textStream << " runningRecursive = true;" << endl; + textStream << " nitpick.runRecursive();" << endl; + textStream << "}" << endl << endl; recursiveTestsFile.close(); } From d96b0534ab539aab1be085f2edf8c06d6b0e3480 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Feb 2019 16:12:13 -0800 Subject: [PATCH 130/130] fix resource texture crash --- .../src/model-networking/TextureCache.cpp | 12 ++++++++---- .../src/model-networking/TextureCache.h | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index c7235337c2..d4cf7e6ce9 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -333,10 +333,14 @@ QSharedPointer TextureCache::createResourceCopy(const QSharedPointer>(); -NetworkTexture::NetworkTexture(const QUrl& url) : +NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) : Resource(url), _maxNumPixels(100) { + if (resourceTexture) { + _textureSource = std::make_shared(url); + _loaded = true; + } } NetworkTexture::NetworkTexture(const NetworkTexture& other) : @@ -1244,11 +1248,11 @@ void ImageReader::read() { Q_ARG(int, texture->getHeight())); } -NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) { +NetworkTexturePointer TextureCache::getResourceTexture(const QUrl& resourceTextureUrl) { gpu::TexturePointer texture; if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) { if (!_spectatorCameraNetworkTexture) { - _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true)); } if (!_spectatorCameraFramebuffer) { getSpectatorCameraFramebuffer(); // initialize frame buffer @@ -1259,7 +1263,7 @@ NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) // FIXME: Generalize this, DRY up this code if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { if (!_hmdPreviewNetworkTexture) { - _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true)); } if (_hmdPreviewFramebuffer) { texture = _hmdPreviewFramebuffer->getRenderBuffer(0); diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index d744d060b6..cdedc64ea5 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -45,7 +45,7 @@ class NetworkTexture : public Resource, public Texture { Q_OBJECT public: - NetworkTexture(const QUrl& url); + NetworkTexture(const QUrl& url, bool resourceTexture = false); NetworkTexture(const NetworkTexture& other); ~NetworkTexture() override; @@ -183,7 +183,7 @@ public: gpu::TexturePointer getTextureByHash(const std::string& hash); gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); - NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); + NetworkTexturePointer getResourceTexture(const QUrl& resourceTextureUrl); const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(int width, int height);