diff --git a/examples/edit.js b/examples/edit.js index 12072cf41f..e034392df0 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -137,6 +137,7 @@ var toolBar = (function () { newSphereButton, newLightButton, newTextButton, + newWebButton, newZoneButton, browseMarketplaceButton; @@ -204,6 +205,16 @@ var toolBar = (function () { alpha: 0.9, visible: false }); + + newWebButton = toolBar.addTool({ + imageURL: "https://s3.amazonaws.com/Oculus/earth17.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + alpha: 0.9, + visible: false + }); + newZoneButton = toolBar.addTool({ imageURL: toolIconUrl + "zonecube_text.svg", subImage: { x: 0, y: 128, width: 128, height: 128 }, @@ -253,6 +264,7 @@ var toolBar = (function () { toolBar.showTool(newSphereButton, doShow); toolBar.showTool(newLightButton, doShow); toolBar.showTool(newTextButton, doShow); + toolBar.showTool(newWebButton, doShow); toolBar.showTool(newZoneButton, doShow); }; @@ -425,6 +437,22 @@ var toolBar = (function () { return true; } + if (newWebButton === toolBar.clicked(clickedOverlay)) { + var position = getPositionToCreateEntity(); + + if (position.x > 0 && position.y > 0 && position.z > 0) { + placingEntityID = Entities.addEntity({ + type: "Web", + position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), + dimensions: { x: 1.6, y: 0.9, z: 0.01 }, + sourceUrl: "https://highfidelity.com/", + }); + } else { + print("Can't create Web Entity: would be out of bounds."); + } + return true; + } + if (newZoneButton === toolBar.clicked(clickedOverlay)) { var position = getPositionToCreateEntity(); diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index 7e6c72712c..4ce787d78a 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -279,6 +279,10 @@ var elModelTextures = document.getElementById("property-model-textures"); var elModelOriginalTextures = document.getElementById("property-model-original-textures"); + var elWebSections = document.querySelectorAll(".web-section"); + allSections.push(elModelSections); + var elWebSourceURL = document.getElementById("property-web-source-url"); + var elTextSections = document.querySelectorAll(".text-section"); allSections.push(elTextSections); var elTextText = document.getElementById("property-text-text"); @@ -468,6 +472,12 @@ elModelAnimationSettings.value = properties.animationSettings; elModelTextures.value = properties.textures; elModelOriginalTextures.value = properties.originalTextures; + } else if (properties.type == "Web") { + for (var i = 0; i < elTextSections.length; i++) { + elWebSections[i].style.display = 'block'; + } + + elWebSourceURL.value = properties.sourceUrl; } else if (properties.type == "Text") { for (var i = 0; i < elTextSections.length; i++) { elTextSections[i].style.display = 'block'; @@ -654,6 +664,8 @@ elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent')); elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff')); + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); + elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); @@ -1019,7 +1031,12 @@ - +
+
Source URL
+
+ +
+
Model URL
diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml new file mode 100644 index 0000000000..0eb943cac7 --- /dev/null +++ b/interface/resources/qml/WebEntity.qml @@ -0,0 +1,10 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebKit 3.0 + +WebView { + id: root + objectName: "webview" + anchors.fill: parent + url: "about:blank" +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b22f1d965e..75bb95c2e2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -341,6 +341,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _domainConnectionRefusals(QList()), _maxOctreePPS(maxOctreePacketsPerSecond.get()) { + setInstance(this); #ifdef Q_OS_WIN installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif @@ -4692,3 +4693,8 @@ void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { int Application::getMaxOctreePacketsPerSecond() { return _maxOctreePPS; } + +qreal Application::getDevicePixelRatio() { + return _window ? _window->windowHandle()->devicePixelRatio() : 1.0; +} + diff --git a/interface/src/Application.h b/interface/src/Application.h index 09edaba341..acd71c2804 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -300,7 +300,7 @@ public: virtual const glm::vec3& getAvatarPosition() const { return _myAvatar->getPosition(); } virtual void overrideEnvironmentData(const EnvironmentData& newData) { _environment.override(newData); } virtual void endOverrideEnvironmentData() { _environment.endOverride(); } - + virtual qreal getDevicePixelRatio(); NodeBounds& getNodeBoundsDisplay() { return _nodeBoundsDisplay; } diff --git a/interface/src/scripting/WebWindowClass.cpp b/interface/src/scripting/WebWindowClass.cpp index be36fe1989..67e5e86280 100644 --- a/interface/src/scripting/WebWindowClass.cpp +++ b/interface/src/scripting/WebWindowClass.cpp @@ -79,7 +79,11 @@ WebWindowClass::WebWindowClass(const QString& title, const QString& url, int wid } _webView->setPage(new DataWebPage()); - _webView->setUrl(url); + if (!url.startsWith("http") && !url.startsWith("file://")) { + _webView->setUrl(QUrl::fromLocalFile(url)); + } else { + _webView->setUrl(url); + } connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index beb8b27e48..cd2e339653 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -35,6 +35,7 @@ #include "RenderableParticleEffectEntityItem.h" #include "RenderableSphereEntityItem.h" #include "RenderableTextEntityItem.h" +#include "RenderableWebEntityItem.h" #include "RenderableZoneEntityItem.h" #include "RenderableLineEntityItem.h" #include "EntitiesRendererLogging.h" @@ -58,6 +59,7 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf REGISTER_ENTITY_TYPE_WITH_FACTORY(Sphere, RenderableSphereEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Light, RenderableLightEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Text, RenderableTextEntityItem::factory) + REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, RenderableWebEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(ParticleEffect, RenderableParticleEffectEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Zone, RenderableZoneEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Line, RenderableLineEntityItem::factory) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp new file mode 100644 index 0000000000..fe85a37762 --- /dev/null +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -0,0 +1,147 @@ +// +// Created by Bradley Austin Davis on 2015/05/12 +// 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 "RenderableWebEntityItem.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int FIXED_FONT_POINT_SIZE = 40; +const float DPI = 30.47; +const float METERS_TO_INCHES = 39.3701; + +EntityItem* RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + return new RenderableWebEntityItem(entityID, properties); +} + +RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : + WebEntityItem(entityItemID, properties) { + qDebug() << "Created web entity " << getID(); +} + +RenderableWebEntityItem::~RenderableWebEntityItem() { + if (_webSurface) { + _webSurface->pause(); + _webSurface->disconnect(_connection); + // After the disconnect, ensure that we have the latest texture by acquiring the + // lock used when updating the _texture value + _textureLock.lock(); + _textureLock.unlock(); + // The lifetime of the QML surface MUST be managed by the main thread + // Additionally, we MUST use local variables copied by value, rather than + // member variables, since they would implicitly refer to a this that + // is no longer valid + auto webSurface = _webSurface; + auto texture = _texture; + AbstractViewStateInterface::instance()->postLambdaEvent([webSurface, texture] { + if (texture) { + webSurface->releaseTexture(texture); + } + webSurface->deleteLater(); + }); + } + qDebug() << "Destroyed web entity " << getID(); +} + +void RenderableWebEntityItem::render(RenderArgs* args) { + QOpenGLContext * currentContext = QOpenGLContext::currentContext(); + QSurface * currentSurface = currentContext->surface(); + if (!_webSurface) { + _webSurface = new OffscreenQmlSurface(); + _webSurface->create(currentContext); + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); + _webSurface->load("WebEntity.qml"); + _webSurface->resume(); + _webSurface->getRootItem()->setProperty("url", _sourceUrl); + _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { + _webSurface->lockTexture(textureId); + assert(!glGetError()); + // TODO change to atomic? + withLock(_textureLock, [&] { + std::swap(_texture, textureId); + }); + if (textureId) { + _webSurface->releaseTexture(textureId); + } + if (_texture) { + _webSurface->makeCurrent(); + glBindTexture(GL_TEXTURE_2D, _texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glBindTexture(GL_TEXTURE_2D, 0); + _webSurface->doneCurrent(); + } + }); + } + + glm::vec2 dims = glm::vec2(_dimensions); + dims *= METERS_TO_INCHES * DPI; + // The offscreen surface is idempotent for resizes (bails early + // if it's a no-op), so it's safe to just call resize every frame + // without worrying about excessive overhead. + _webSurface->resize(QSize(dims.x, dims.y)); + currentContext->makeCurrent(currentSurface); + + PerformanceTimer perfTimer("RenderableWebEntityItem::render"); + assert(getType() == EntityTypes::Web); + glm::vec3 position = getPosition(); + glm::vec3 dimensions = getDimensions(); + glm::vec3 halfDimensions = dimensions / 2.0f; + glm::quat rotation = getRotation(); + + //qCDebug(entitytree) << "RenderableWebEntityItem::render() id:" << getEntityItemID() << "text:" << getText(); + + glPushMatrix(); + { + glTranslatef(position.x, position.y, position.z); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + + float alpha = 1.0f; + static const glm::vec2 texMin(0); + static const glm::vec2 texMax(1); + glm::vec2 topLeft(-halfDimensions.x, -halfDimensions.y); + glm::vec2 bottomRight(halfDimensions.x, halfDimensions.y); + if (_texture) { + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, _texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + } + DependencyManager::get()->renderQuad( + topLeft, bottomRight, texMin, texMax, glm::vec4(1)); + if (_texture) { + glBindTexture(GL_TEXTURE_2D, 0); + glEnable(GL_TEXTURE_2D); + } + } + glPopMatrix(); +} + +void RenderableWebEntityItem::setSourceUrl(const QString& value) { + if (_sourceUrl != value) { + _sourceUrl = value; + if (_webSurface) { + AbstractViewStateInterface::instance()->postLambdaEvent([this] { + _webSurface->getRootItem()->setProperty("url", _sourceUrl); + }); + } + } +} diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h new file mode 100644 index 0000000000..cf63d7915e --- /dev/null +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -0,0 +1,36 @@ +// +// Created by Bradley Austin Davis on 2015/05/12 +// 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_RenderableWebEntityItem_h +#define hifi_RenderableWebEntityItem_h + +#include + +#include + +class OffscreenQmlSurface; + +class RenderableWebEntityItem : public WebEntityItem { +public: + static EntityItem* factory(const EntityItemID& entityID, const EntityItemProperties& properties); + + RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); + ~RenderableWebEntityItem(); + + virtual void render(RenderArgs* args); + virtual void setSourceUrl(const QString& value); + +private: + OffscreenQmlSurface* _webSurface{ nullptr }; + QMetaObject::Connection _connection; + uint32_t _texture{ 0 }; + QMutex _textureLock; +}; + + +#endif // hifi_RenderableWebEntityItem_h diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ff54ef88fe..0d997cd7cb 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -86,6 +86,7 @@ EntityItemProperties::EntityItemProperties() : CONSTRUCT_PROPERTY(keyLightDirection, ZoneEntityItem::DEFAULT_KEYLIGHT_DIRECTION), CONSTRUCT_PROPERTY(name, ENTITY_ITEM_DEFAULT_NAME), CONSTRUCT_PROPERTY(backgroundMode, BACKGROUND_MODE_INHERIT), + CONSTRUCT_PROPERTY(sourceUrl, ""), _id(UNKNOWN_ENTITY_ID), _idSet(false), @@ -328,6 +329,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_AMBIENT_INTENSITY, keyLightAmbientIntensity); CHECK_PROPERTY_CHANGE(PROP_KEYLIGHT_DIRECTION, keyLightDirection); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); + CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); changedProperties += _stage.getChangedProperties(); changedProperties += _atmosphere.getChangedProperties(); @@ -409,6 +411,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(keyLightAmbientIntensity); COPY_PROPERTY_TO_QSCRIPTVALUE_VEC3(keyLightDirection); COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(backgroundMode, getBackgroundModeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(sourceUrl); // Sitting properties support if (!skipDefaults) { @@ -510,6 +513,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object) { COPY_PROPERTY_FROM_QSCRIPTVALUE_FLOAT(keyLightAmbientIntensity, setKeyLightAmbientIntensity); COPY_PROPERTY_FROM_QSCRIPTVALUE_VEC3(keyLightDirection, setKeyLightDirection); COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(backgroundMode, BackgroundMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_STRING(sourceUrl, setSourceUrl); _stage.copyFromScriptValue(object, _defaultSettings); _atmosphere.copyFromScriptValue(object, _defaultSettings); @@ -666,7 +670,11 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_LOCKED, appendValue, properties.getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, appendValue, properties.getUserData()); APPEND_ENTITY_PROPERTY(PROP_SIMULATOR_ID, appendValue, properties.getSimulatorID()); - + + if (properties.getType() == EntityTypes::Web) { + APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, appendValue, properties.getSourceUrl()); + } + if (properties.getType() == EntityTypes::Text) { APPEND_ENTITY_PROPERTY(PROP_TEXT, appendValue, properties.getText()); APPEND_ENTITY_PROPERTY(PROP_LINE_HEIGHT, appendValue, properties.getLineHeight()); @@ -922,6 +930,10 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_USER_DATA, setUserData); READ_ENTITY_PROPERTY_UUID_TO_PROPERTIES(PROP_SIMULATOR_ID, setSimulatorID); + if (properties.getType() == EntityTypes::Web) { + READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_SOURCE_URL, setSourceUrl); + } + if (properties.getType() == EntityTypes::Text) { READ_ENTITY_PROPERTY_STRING_TO_PROPERTIES(PROP_TEXT, setText); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_HEIGHT, float, setLineHeight); @@ -1073,6 +1085,7 @@ void EntityItemProperties::markAllChanged() { _atmosphere.markAllChanged(); _skybox.markAllChanged(); + _sourceUrlChanged = true; } /// The maximum bounding cube for the entity, independent of it's rotation. diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 956065d069..a1ca4d5eff 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -52,6 +52,7 @@ class EntityItemProperties { friend class TextEntityItem; // TODO: consider removing this friend relationship and use public methods friend class ParticleEffectEntityItem; // TODO: consider removing this friend relationship and use public methods friend class ZoneEntityItem; // TODO: consider removing this friend relationship and use public methods + friend class WebEntityItem; // TODO: consider removing this friend relationship and use public methods friend class LineEntityItem; // TODO: consider removing this friend relationship and use public methods public: EntityItemProperties(); @@ -139,6 +140,7 @@ public: DEFINE_PROPERTY_GROUP(Stage, stage, StagePropertyGroup); DEFINE_PROPERTY_GROUP(Atmosphere, atmosphere, AtmospherePropertyGroup); DEFINE_PROPERTY_GROUP(Skybox, skybox, SkyboxPropertyGroup); + DEFINE_PROPERTY_REF(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString); static QString getBackgroundModeString(BackgroundMode mode); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 93b8c76216..cf7fc671a0 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -148,6 +148,10 @@ enum EntityPropertyList { PROP_SKYBOX_URL = PROP_ANIMATION_FPS, PROP_STAGE_AUTOMATIC_HOURDAY = PROP_ANIMATION_FRAME_INDEX, + // Aliases/Piggyback properties for Web. These properties intentionally reuse the enum values for + // other properties which will never overlap with each other. + PROP_SOURCE_URL = PROP_MODEL_URL, + // WARNING!!! DO NOT ADD PROPS_xxx here unless you really really meant to.... Add them UP above }; diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index a9eee5da90..794a77b194 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -24,6 +24,7 @@ #include "ParticleEffectEntityItem.h" #include "SphereEntityItem.h" #include "TextEntityItem.h" +#include "WebEntityItem.h" #include "ZoneEntityItem.h" #include "LineEntityItem.h" @@ -37,6 +38,7 @@ const QString ENTITY_TYPE_NAME_UNKNOWN = "Unknown"; // Register Entity the default implementations of entity types here... REGISTER_ENTITY_TYPE(Model) REGISTER_ENTITY_TYPE(Box) +REGISTER_ENTITY_TYPE(Web) REGISTER_ENTITY_TYPE(Sphere) REGISTER_ENTITY_TYPE(Light) REGISTER_ENTITY_TYPE(Text) diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 28cce52778..b3de3dfc8e 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -37,6 +37,7 @@ public: Text, ParticleEffect, Zone, + Web, Line, LAST = Line } EntityType; diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp new file mode 100644 index 0000000000..90dd13334d --- /dev/null +++ b/libraries/entities/src/WebEntityItem.cpp @@ -0,0 +1,153 @@ +// +// Created by Bradley Austin Davis on 2015/05/12 +// 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 "WebEntityItem.h" + +#include + +#include + +#include +#include + +#include "EntityTree.h" +#include "EntityTreeElement.h" +#include "EntitiesLogging.h" + + +const QString WebEntityItem::DEFAULT_SOURCE_URL("http://www.google.com"); + +EntityItem* WebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + EntityItem* result = new WebEntityItem(entityID, properties); + return result; +} + +WebEntityItem::WebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : + EntityItem(entityItemID) +{ + _type = EntityTypes::Web; + _created = properties.getCreated(); + setProperties(properties); +} + +const float WEB_ENTITY_ITEM_FIXED_DEPTH = 0.01f; + +void WebEntityItem::setDimensions(const glm::vec3& value) { + // NOTE: Web Entities always have a "depth" of 1cm. + _dimensions = glm::vec3(value.x, value.y, WEB_ENTITY_ITEM_FIXED_DEPTH); +} + +EntityItemProperties WebEntityItem::getProperties() const { + EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class + COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); + return properties; +} + +bool WebEntityItem::setProperties(const EntityItemProperties& properties) { + bool somethingChanged = false; + somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(sourceUrl, setSourceUrl); + + if (somethingChanged) { + bool wantDebug = false; + if (wantDebug) { + uint64_t now = usecTimestampNow(); + int elapsed = now - getLastEdited(); + qCDebug(entities) << "WebEntityItem::setProperties() AFTER update... edited AGO=" << elapsed << + "now=" << now << " getLastEdited()=" << getLastEdited(); + } + setLastEdited(properties._lastEdited); + } + + return somethingChanged; +} + +int WebEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData) { + + int bytesRead = 0; + const unsigned char* dataAt = data; + + READ_ENTITY_PROPERTY_STRING(PROP_SOURCE_URL, setSourceUrl); + + return bytesRead; +} + + +// TODO: eventually only include properties changed since the params.lastViewFrustumSent time +EntityPropertyFlags WebEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { + EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + requestedProperties += PROP_SOURCE_URL; + return requestedProperties; +} + +void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const { + + bool successPropertyFits = true; + APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, appendValue, _sourceUrl); +} + + +bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, + void** intersectedObject, bool precisionPicking) const { + + RayIntersectionInfo rayInfo; + rayInfo._rayStart = origin; + rayInfo._rayDirection = direction; + rayInfo._rayLength = std::numeric_limits::max(); + + PlaneShape plane; + + const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); + glm::vec3 normal = _rotation * UNROTATED_NORMAL; + plane.setNormal(normal); + plane.setPoint(getPosition()); // the position is definitely a point on our plane + + bool intersects = plane.findRayIntersection(rayInfo); + + if (intersects) { + glm::vec3 hitAt = origin + (direction * rayInfo._hitDistance); + // now we know the point the ray hit our plane + + glm::mat4 rotation = glm::mat4_cast(getRotation()); + glm::mat4 translation = glm::translate(getPosition()); + glm::mat4 entityToWorldMatrix = translation * rotation; + glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); + + glm::vec3 dimensions = getDimensions(); + glm::vec3 registrationPoint = getRegistrationPoint(); + glm::vec3 corner = -(dimensions * registrationPoint); + AABox entityFrameBox(corner, dimensions); + + glm::vec3 entityFrameHitAt = glm::vec3(worldToEntityMatrix * glm::vec4(hitAt, 1.0f)); + + intersects = entityFrameBox.contains(entityFrameHitAt); + } + + if (intersects) { + distance = rayInfo._hitDistance; + } + return intersects; +} + +void WebEntityItem::setSourceUrl(const QString& value) { + if (_sourceUrl != value) { + _sourceUrl = value; + } +} + +const QString& WebEntityItem::getSourceUrl() const { return _sourceUrl; } \ No newline at end of file diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h new file mode 100644 index 0000000000..35e98b2092 --- /dev/null +++ b/libraries/entities/src/WebEntityItem.h @@ -0,0 +1,59 @@ +// +// Created by Bradley Austin Davis on 2015/05/12 +// 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_WebEntityItem_h +#define hifi_WebEntityItem_h + +#include "EntityItem.h" + +class WebEntityItem : public EntityItem { +public: + static const QString DEFAULT_SOURCE_URL; + + static EntityItem* factory(const EntityItemID& entityID, const EntityItemProperties& properties); + + WebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); + + ALLOW_INSTANTIATION // This class can be instantiated + + /// set dimensions in domain scale units (0.0 - 1.0) this will also reset radius appropriately + virtual void setDimensions(const glm::vec3& value); + virtual ShapeType getShapeType() const { return SHAPE_TYPE_BOX; } + + // methods for getting/setting all properties of an entity + virtual EntityItemProperties getProperties() const; + virtual bool setProperties(const EntityItemProperties& properties); + + // TODO: eventually only include properties changed since the params.lastViewFrustumSent time + virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; + + virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const; + + virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData); + + virtual bool supportsDetailedRayIntersection() const { return true; } + virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, + void** intersectedObject, bool precisionPicking) const; + + virtual void setSourceUrl(const QString& value); + const QString& getSourceUrl() const; + +protected: + QString _sourceUrl; +}; + +#endif // hifi_WebEntityItem_h diff --git a/libraries/render-utils/src/AbstractViewStateInterface.cpp b/libraries/render-utils/src/AbstractViewStateInterface.cpp new file mode 100644 index 0000000000..bd4ba8b1b6 --- /dev/null +++ b/libraries/render-utils/src/AbstractViewStateInterface.cpp @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis 2015/05/13 +// 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 "AbstractViewStateInterface.h" + +static AbstractViewStateInterface* INSTANCE{nullptr}; + +AbstractViewStateInterface* AbstractViewStateInterface::instance() { + return INSTANCE; +} + +void AbstractViewStateInterface::setInstance(AbstractViewStateInterface* instance) { + INSTANCE = instance; +} + diff --git a/libraries/render-utils/src/AbstractViewStateInterface.h b/libraries/render-utils/src/AbstractViewStateInterface.h index 8ba8ba33c7..a1447293b7 100644 --- a/libraries/render-utils/src/AbstractViewStateInterface.h +++ b/libraries/render-utils/src/AbstractViewStateInterface.h @@ -13,6 +13,9 @@ #define hifi_AbstractViewStateInterface_h #include +#include + +#include class Transform; class QThread; @@ -37,7 +40,7 @@ public: /// overrides environment data virtual void overrideEnvironmentData(const EnvironmentData& newData) = 0; virtual void endOverrideEnvironmentData() = 0; - + /// gets the shadow view frustum for rendering the view state virtual ViewFrustum* getShadowViewFrustum() = 0; @@ -53,6 +56,12 @@ public: virtual PickRay computePickRay(float x, float y) const = 0; virtual const glm::vec3& getAvatarPosition() const = 0; + + virtual void postLambdaEvent(std::function f) = 0; + virtual qreal getDevicePixelRatio() = 0; + + static AbstractViewStateInterface* instance(); + static void setInstance(AbstractViewStateInterface* instance); }; diff --git a/libraries/render-utils/src/OffscreenQmlSurface.cpp b/libraries/render-utils/src/OffscreenQmlSurface.cpp new file mode 100644 index 0000000000..5b39610640 --- /dev/null +++ b/libraries/render-utils/src/OffscreenQmlSurface.cpp @@ -0,0 +1,376 @@ +// +// Created by Bradley Austin Davis on 2015-05-13 +// 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 "OffscreenQmlSurface.h" + +#include +#include +#include +#include +#include "AbstractViewStateInterface.h" + +Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) +Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") + +// Time between receiving a request to render the offscreen UI actually triggering +// the render. Could possibly be increased depending on the framerate we expect to +// achieve. +static const int SMALL_INTERVAL = 5; + +OffscreenQmlSurface::OffscreenQmlSurface() { +} + +OffscreenQmlSurface::~OffscreenQmlSurface() { + // Make sure the context is current while doing cleanup. Note that we use the + // offscreen surface here because passing 'this' at this point is not safe: the + // underlying platform window may already be destroyed. To avoid all the trouble, use + // another surface that is valid for sure. + makeCurrent(); + + // Delete the render control first since it will free the scenegraph resources. + // Destroy the QQuickWindow only afterwards. + delete _renderControl; + + delete _qmlComponent; + delete _quickWindow; + delete _qmlEngine; + + doneCurrent(); +} + +void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { + OffscreenGlCanvas::create(shareContext); + + makeCurrent(); + + // Create a QQuickWindow that is associated with out render control. Note that this + // window never gets created or shown, meaning that it will never get an underlying + // native (platform) window. + QQuickWindow::setDefaultAlphaBuffer(true); + _quickWindow = new QQuickWindow(_renderControl); + _quickWindow->setColor(QColor(255, 255, 255, 0)); + _quickWindow->setFlags(_quickWindow->flags() | static_cast(Qt::WA_TranslucentBackground)); + // Create a QML engine. + _qmlEngine = new QQmlEngine; + if (!_qmlEngine->incubationController()) { + _qmlEngine->setIncubationController(_quickWindow->incubationController()); + } + + // When Quick says there is a need to render, we will not render immediately. Instead, + // a timer with a small interval is used to get better performance. + _updateTimer.setSingleShot(true); + _updateTimer.setInterval(SMALL_INTERVAL); + connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); + + // Now hook up the signals. For simplicy we don't differentiate between + // renderRequested (only render is needed, no sync) and sceneChanged (polish and sync + // is needed too). + connect(_renderControl, &QQuickRenderControl::renderRequested, this, &OffscreenQmlSurface::requestRender); + connect(_renderControl, &QQuickRenderControl::sceneChanged, this, &OffscreenQmlSurface::requestUpdate); + +#ifdef DEBUG + connect(_quickWindow, &QQuickWindow::focusObjectChanged, [this]{ + qCDebug(offscreenFocus) << "New focus item " << _quickWindow->focusObject(); + }); + connect(_quickWindow, &QQuickWindow::activeFocusItemChanged, [this] { + qCDebug(offscreenFocus) << "New active focus item " << _quickWindow->activeFocusItem(); + }); +#endif + + _qmlComponent = new QQmlComponent(_qmlEngine); + // Initialize the render control and our OpenGL resources. + makeCurrent(); + _renderControl->initialize(&_context); +} + +void OffscreenQmlSurface::resize(const QSize& newSize) { + // Qt bug in 5.4 forces this check of pixel ratio, + // even though we're rendering offscreen. + qreal pixelRatio = 1.0; + if (_renderControl && _renderControl->_renderWindow) { + pixelRatio = _renderControl->_renderWindow->devicePixelRatio(); + } else { + pixelRatio = AbstractViewStateInterface::instance()->getDevicePixelRatio(); + } + QSize newOffscreenSize = newSize * pixelRatio; + if (newOffscreenSize == _fboCache.getSize()) { + return; + } + + // Clear out any fbos with the old size + makeCurrent(); + qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; + _fboCache.setSize(newSize * pixelRatio); + + if (_quickWindow) { + _quickWindow->setGeometry(QRect(QPoint(), newSize)); + _quickWindow->contentItem()->setSize(newSize); + } + + + // Update our members + if (_rootItem) { + _rootItem->setSize(newSize); + } + + doneCurrent(); +} + +QQuickItem* OffscreenQmlSurface::getRootItem() { + return _rootItem; +} + +void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { + _qmlEngine->setBaseUrl(baseUrl); +} + +QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function f) { + _qmlComponent->loadUrl(qmlSource); + if (_qmlComponent->isLoading()) { + connect(_qmlComponent, &QQmlComponent::statusChanged, this, + [this, f](QQmlComponent::Status){ + finishQmlLoad(f); + }); + return nullptr; + } + + return finishQmlLoad(f); +} + +void OffscreenQmlSurface::requestUpdate() { + _polish = true; + if (!_updateTimer.isActive()) { + _updateTimer.start(); + } +} + +void OffscreenQmlSurface::requestRender() { + if (!_updateTimer.isActive()) { + _updateTimer.start(); + } +} + +QObject* OffscreenQmlSurface::finishQmlLoad(std::function f) { + disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); + if (_qmlComponent->isError()) { + QList errorList = _qmlComponent->errors(); + foreach(const QQmlError& error, errorList) { + qWarning() << error.url() << error.line() << error; + } + return nullptr; + } + + QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); + QObject* newObject = _qmlComponent->beginCreate(newContext); + if (_qmlComponent->isError()) { + QList errorList = _qmlComponent->errors(); + foreach(const QQmlError& error, errorList) + qWarning() << error.url() << error.line() << error; + if (!_rootItem) { + qFatal("Unable to finish loading QML root"); + } + return nullptr; + } + + f(newContext, newObject); + _qmlComponent->completeCreate(); + + + // All quick items should be focusable + QQuickItem* newItem = qobject_cast(newObject); + if (newItem) { + // Make sure we make items focusable (critical for + // supporting keyboard shortcuts) + newItem->setFlag(QQuickItem::ItemIsFocusScope, true); + } + + // If we already have a root, just set a couple of flags and the ancestry + if (_rootItem) { + // Allow child windows to be destroyed from JS + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(_rootItem); + if (newItem) { + newItem->setParentItem(_rootItem); + } + return newObject; + } + + if (!newItem) { + qFatal("Could not load object as root item"); + return nullptr; + } + // The root item is ready. Associate it with the window. + _rootItem = newItem; + _rootItem->setParentItem(_quickWindow->contentItem()); + _rootItem->setSize(_quickWindow->renderTargetSize()); + return _rootItem; +} + + +void OffscreenQmlSurface::updateQuick() { + if (_paused) { + return; + } + if (!makeCurrent()) { + return; + } + + // Polish, synchronize and render the next frame (into our fbo). In this example + // everything happens on the same thread and therefore all three steps are performed + // in succession from here. In a threaded setup the render() call would happen on a + // separate thread. + if (_polish) { + _renderControl->polishItems(); + _renderControl->sync(); + _polish = false; + } + + QOpenGLFramebufferObject* fbo = _fboCache.getReadyFbo(); + + _quickWindow->setRenderTarget(fbo); + fbo->bind(); + + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + _renderControl->render(); + // FIXME The web browsers seem to be leaving GL in an error state. + // Need a debug context with sync logging to figure out why. + // for now just clear the errors + glGetError(); +// Q_ASSERT(!glGetError()); + + _quickWindow->resetOpenGLState(); + + QOpenGLFramebufferObject::bindDefault(); + // Force completion of all the operations before we emit the texture as being ready for use + glFinish(); + + emit textureUpdated(fbo->texture()); +} + +QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { + vec2 sourceSize; + if (dynamic_cast(sourceObject)) { + sourceSize = toGlm(((QWidget*)sourceObject)->size()); + } else if (dynamic_cast(sourceObject)) { + sourceSize = toGlm(((QWindow*)sourceObject)->size()); + } + vec2 offscreenPosition = toGlm(sourcePosition); + offscreenPosition /= sourceSize; + offscreenPosition *= vec2(toGlm(_quickWindow->size())); + return QPointF(offscreenPosition.x, offscreenPosition.y); +} + +/////////////////////////////////////////////////////// +// +// Event handling customization +// + +bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) { + // Only intercept events while we're in an active state + if (_paused) { + return false; + } + +#ifdef DEBUG + // Don't intercept our own events, or we enter an infinite recursion + QObject* recurseTest = originalDestination; + while (recurseTest) { + Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); + recurseTest = recurseTest->parent(); + } +#endif + + + switch (event->type()) { + case QEvent::Resize: { + QResizeEvent* resizeEvent = static_cast(event); + QGLWidget* widget = dynamic_cast(originalDestination); + if (widget) { + this->resize(resizeEvent->size()); + } + break; + } + + case QEvent::KeyPress: + case QEvent::KeyRelease: { + event->ignore(); + if (QCoreApplication::sendEvent(_quickWindow, event)) { + return event->isAccepted(); + } + break; + } + + case QEvent::Wheel: { + QWheelEvent* wheelEvent = static_cast(event); + QWheelEvent mappedEvent( + mapWindowToUi(wheelEvent->pos(), originalDestination), + wheelEvent->delta(), wheelEvent->buttons(), + wheelEvent->modifiers(), wheelEvent->orientation()); + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + + // Fall through + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: { + QMouseEvent* mouseEvent = static_cast(event); + QPointF originalPos = mouseEvent->localPos(); + QPointF transformedPos = _mouseTranslator(originalPos); + transformedPos = mapWindowToUi(transformedPos, originalDestination); + QMouseEvent mappedEvent(mouseEvent->type(), + transformedPos, + mouseEvent->screenPos(), mouseEvent->button(), + mouseEvent->buttons(), mouseEvent->modifiers()); + if (event->type() == QEvent::MouseMove) { + _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); + } + mappedEvent.ignore(); + if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { + return mappedEvent.isAccepted(); + } + break; + } + + default: + break; + } + + return false; +} + +void OffscreenQmlSurface::lockTexture(int texture) { + _fboCache.lockTexture(texture); +} + +void OffscreenQmlSurface::releaseTexture(int texture) { + _fboCache.releaseTexture(texture); +} + +void OffscreenQmlSurface::pause() { + _paused = true; +} + +void OffscreenQmlSurface::resume() { + _paused = false; + requestRender(); +} + +bool OffscreenQmlSurface::isPaused() const { + return _paused; +} + +void OffscreenQmlSurface::setProxyWindow(QWindow* window) { + _renderControl->_renderWindow = window; +} + diff --git a/libraries/render-utils/src/OffscreenQmlSurface.h b/libraries/render-utils/src/OffscreenQmlSurface.h new file mode 100644 index 0000000000..dcb3e1e17e --- /dev/null +++ b/libraries/render-utils/src/OffscreenQmlSurface.h @@ -0,0 +1,108 @@ +// +// Created by Bradley Austin Davis on 2015-04-04 +// 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 +// +#pragma once +#ifndef hifi_OffscreenQmlSurface_h +#define hifi_OffscreenQmlSurface_h + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "OffscreenGlCanvas.h" +#include "FboCache.h" + +class OffscreenQmlSurface : public OffscreenGlCanvas { + Q_OBJECT +protected: + class QMyQuickRenderControl : public QQuickRenderControl { + protected: + QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ + if (nullptr == _renderWindow) { + return QQuickRenderControl::renderWindow(offset); + } + if (nullptr != offset) { + offset->rx() = offset->ry() = 0; + } + return _renderWindow; + } + + private: + QWindow* _renderWindow{ nullptr }; + friend class OffscreenQmlSurface; + }; +public: + OffscreenQmlSurface(); + virtual ~OffscreenQmlSurface(); + + using MouseTranslator = std::function; + + void create(QOpenGLContext* context); + void resize(const QSize& size); + QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + return load(QUrl(qmlSourceFile), f); + } + + // Optional values for event handling + void setProxyWindow(QWindow* window); + void setMouseTranslator(MouseTranslator mouseTranslator) { + _mouseTranslator = mouseTranslator; + } + + void pause(); + void resume(); + bool isPaused() const; + + void setBaseUrl(const QUrl& baseUrl); + QQuickItem* getRootItem(); + + virtual bool eventFilter(QObject* originalDestination, QEvent* event); + +signals: + void textureUpdated(GLuint texture); + +public slots: + void requestUpdate(); + void requestRender(); + void lockTexture(int texture); + void releaseTexture(int texture); + +private: + QObject* finishQmlLoad(std::function f); + QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); + +private slots: + void updateQuick(); + +protected: + QQuickWindow* _quickWindow{ nullptr }; + +private: + QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; + QQmlEngine* _qmlEngine{ nullptr }; + QQmlComponent* _qmlComponent{ nullptr }; + QQuickItem* _rootItem{ nullptr }; + QTimer _updateTimer; + FboCache _fboCache; + bool _polish{ true }; + bool _paused{ true }; + MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; +}; + +#endif diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 7d13a00324..449657ca04 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -16,14 +16,6 @@ #include "MessageDialog.h" -Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) -Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") - -// Time between receiving a request to render the offscreen UI actually triggering -// the render. Could possibly be increased depending on the framerate we expect to -// achieve. -static const int SMALL_INTERVAL = 5; - class OffscreenUiRoot : public QQuickItem { Q_OBJECT public: @@ -36,6 +28,25 @@ public: }; + +// This hack allows the QML UI to work with keys that are also bound as +// shortcuts at the application level. However, it seems as though the +// bound actions are still getting triggered. At least for backspace. +// Not sure why. +// +// However, the problem may go away once we switch to the new menu system, +// so I think it's OK for the time being. +bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { + Q_ASSERT(event->type() == QEvent::ShortcutOverride); + QObject* focusObject = _quickWindow->focusObject(); + if (focusObject != _quickWindow && focusObject != getRootItem()) { + //qDebug() << "Swallowed shortcut " << static_cast(event)->key(); + event->accept(); + return true; + } + return false; +} + OffscreenUiRoot::OffscreenUiRoot(QQuickItem* parent) : QQuickItem(parent) { } @@ -48,378 +59,15 @@ OffscreenUi::OffscreenUi() { } OffscreenUi::~OffscreenUi() { - // Make sure the context is current while doing cleanup. Note that we use the - // offscreen surface here because passing 'this' at this point is not safe: the - // underlying platform window may already be destroyed. To avoid all the trouble, use - // another surface that is valid for sure. - makeCurrent(); - - // Delete the render control first since it will free the scenegraph resources. - // Destroy the QQuickWindow only afterwards. - delete _renderControl; - - delete _qmlComponent; - delete _quickWindow; - delete _qmlEngine; - - doneCurrent(); } -void OffscreenUi::create(QOpenGLContext* shareContext) { - OffscreenGlCanvas::create(shareContext); - - makeCurrent(); - - // Create a QQuickWindow that is associated with out render control. Note that this - // window never gets created or shown, meaning that it will never get an underlying - // native (platform) window. - QQuickWindow::setDefaultAlphaBuffer(true); - _quickWindow = new QQuickWindow(_renderControl); - _quickWindow->setColor(QColor(255, 255, 255, 0)); - _quickWindow->setFlags(_quickWindow->flags() | static_cast(Qt::WA_TranslucentBackground)); - // Create a QML engine. - _qmlEngine = new QQmlEngine; - if (!_qmlEngine->incubationController()) { - _qmlEngine->setIncubationController(_quickWindow->incubationController()); - } - - // When Quick says there is a need to render, we will not render immediately. Instead, - // a timer with a small interval is used to get better performance. - _updateTimer.setSingleShot(true); - _updateTimer.setInterval(SMALL_INTERVAL); - connect(&_updateTimer, &QTimer::timeout, this, &OffscreenUi::updateQuick); - - // Now hook up the signals. For simplicy we don't differentiate between - // renderRequested (only render is needed, no sync) and sceneChanged (polish and sync - // is needed too). - connect(_renderControl, &QQuickRenderControl::renderRequested, this, &OffscreenUi::requestRender); - connect(_renderControl, &QQuickRenderControl::sceneChanged, this, &OffscreenUi::requestUpdate); - -#ifdef DEBUG - connect(_quickWindow, &QQuickWindow::focusObjectChanged, [this]{ - qCDebug(offscreenFocus) << "New focus item " << _quickWindow->focusObject(); - }); - connect(_quickWindow, &QQuickWindow::activeFocusItemChanged, [this] { - qCDebug(offscreenFocus) << "New active focus item " << _quickWindow->activeFocusItem(); - }); -#endif - - _qmlComponent = new QQmlComponent(_qmlEngine); - // Initialize the render control and our OpenGL resources. - makeCurrent(); - _renderControl->initialize(&_context); -} - -void OffscreenUi::addImportPath(const QString& path) { - _qmlEngine->addImportPath(path); -} - -void OffscreenUi::resize(const QSize& newSize) { - makeCurrent(); - - qreal pixelRatio = _renderControl->_renderWindow ? _renderControl->_renderWindow->devicePixelRatio() : 1.0; - QSize newOffscreenSize = newSize * pixelRatio; - if (newOffscreenSize == _fboCache.getSize()) { - return; - } - - // Clear out any fbos with the old size - qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; - _fboCache.setSize(newSize * pixelRatio); - - if (_quickWindow) { - _quickWindow->setGeometry(QRect(QPoint(), newSize)); - _quickWindow->contentItem()->setSize(newSize); - } - - - // Update our members - if (_rootItem) { - _rootItem->setSize(newSize); - } - - doneCurrent(); -} - -QQuickItem* OffscreenUi::getRootItem() { - return _rootItem; -} - -void OffscreenUi::setBaseUrl(const QUrl& baseUrl) { - _qmlEngine->setBaseUrl(baseUrl); -} - -QObject* OffscreenUi::load(const QUrl& qmlSource, std::function f) { - _qmlComponent->loadUrl(qmlSource); - if (_qmlComponent->isLoading()) { - connect(_qmlComponent, &QQmlComponent::statusChanged, this, - [this, f](QQmlComponent::Status){ - finishQmlLoad(f); - }); - return nullptr; - } - - return finishQmlLoad(f); -} - -void OffscreenUi::requestUpdate() { - _polish = true; - if (!_updateTimer.isActive()) { - _updateTimer.start(); - } -} - -void OffscreenUi::requestRender() { - if (!_updateTimer.isActive()) { - _updateTimer.start(); - } -} - -QObject* OffscreenUi::finishQmlLoad(std::function f) { - disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) { - qWarning() << error.url() << error.line() << error; - } - return nullptr; - } - - QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); - QObject* newObject = _qmlComponent->beginCreate(newContext); - if (_qmlComponent->isError()) { - QList errorList = _qmlComponent->errors(); - foreach(const QQmlError& error, errorList) - qWarning() << error.url() << error.line() << error; - if (!_rootItem) { - qFatal("Unable to finish loading QML root"); - } - return nullptr; - } - - f(newContext, newObject); - _qmlComponent->completeCreate(); - - - // All quick items should be focusable - QQuickItem* newItem = qobject_cast(newObject); - if (newItem) { - // Make sure we make items focusable (critical for - // supporting keyboard shortcuts) - newItem->setFlag(QQuickItem::ItemIsFocusScope, true); - } - - // If we already have a root, just set a couple of flags and the ancestry - if (_rootItem) { - // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); - newObject->setParent(_rootItem); - if (newItem) { - newItem->setParentItem(_rootItem); - } - return newObject; - } - - if (!newItem) { - qFatal("Could not load object as root item"); - return nullptr; - } - // The root item is ready. Associate it with the window. - _rootItem = newItem; - _rootItem->setParentItem(_quickWindow->contentItem()); - _rootItem->setSize(_quickWindow->renderTargetSize()); - return _rootItem; -} - - -void OffscreenUi::updateQuick() { - if (_paused) { - return; - } - if (!makeCurrent()) { - return; - } - - // Polish, synchronize and render the next frame (into our fbo). In this example - // everything happens on the same thread and therefore all three steps are performed - // in succession from here. In a threaded setup the render() call would happen on a - // separate thread. - if (_polish) { - _renderControl->polishItems(); - _renderControl->sync(); - _polish = false; - } - - QOpenGLFramebufferObject* fbo = _fboCache.getReadyFbo(); - - _quickWindow->setRenderTarget(fbo); - fbo->bind(); - - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - _renderControl->render(); - // FIXME The web browsers seem to be leaving GL in an error state. - // Need a debug context with sync logging to figure out why. - // for now just clear the errors - glGetError(); -// Q_ASSERT(!glGetError()); - - _quickWindow->resetOpenGLState(); - - QOpenGLFramebufferObject::bindDefault(); - // Force completion of all the operations before we emit the texture as being ready for use - glFinish(); - - emit textureUpdated(fbo->texture()); -} - -QPointF OffscreenUi::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) { - vec2 sourceSize; - if (dynamic_cast(sourceObject)) { - sourceSize = toGlm(((QWidget*)sourceObject)->size()); - } else if (dynamic_cast(sourceObject)) { - sourceSize = toGlm(((QWindow*)sourceObject)->size()); - } - vec2 offscreenPosition = toGlm(sourcePosition); - offscreenPosition /= sourceSize; - offscreenPosition *= vec2(toGlm(_quickWindow->size())); - return QPointF(offscreenPosition.x, offscreenPosition.y); -} - -// This hack allows the QML UI to work with keys that are also bound as -// shortcuts at the application level. However, it seems as though the -// bound actions are still getting triggered. At least for backspace. -// Not sure why. -// -// However, the problem may go away once we switch to the new menu system, -// so I think it's OK for the time being. -bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { - Q_ASSERT(event->type() == QEvent::ShortcutOverride); - QObject* focusObject = _quickWindow->focusObject(); - if (focusObject != _quickWindow && focusObject != _rootItem) { - //qDebug() << "Swallowed shortcut " << static_cast(event)->key(); - event->accept(); - return true; - } - return false; -} - -/////////////////////////////////////////////////////// -// -// Event handling customization -// - -bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { - // Only intercept events while we're in an active state - if (_paused) { - return false; - } - - -#ifdef DEBUG - // Don't intercept our own events, or we enter an infinite recursion - QObject* recurseTest = originalDestination; - while (recurseTest) { - Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); - recurseTest = recurseTest->parent(); - } -#endif - - - switch (event->type()) { - case QEvent::Resize: { - QResizeEvent* resizeEvent = static_cast(event); - QGLWidget* widget = dynamic_cast(originalDestination); - if (widget) { - this->resize(resizeEvent->size()); - } - break; - } - - case QEvent::KeyPress: - case QEvent::KeyRelease: { - event->ignore(); - if (QCoreApplication::sendEvent(_quickWindow, event)) { - return event->isAccepted(); - } - break; - } - - case QEvent::Wheel: { - QWheelEvent* wheelEvent = static_cast(event); - QWheelEvent mappedEvent( - mapWindowToUi(wheelEvent->pos(), originalDestination), - wheelEvent->delta(), wheelEvent->buttons(), - wheelEvent->modifiers(), wheelEvent->orientation()); - mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { - return mappedEvent.isAccepted(); - } - break; - } - - // Fall through - case QEvent::MouseButtonDblClick: - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - case QEvent::MouseMove: { - QMouseEvent* mouseEvent = static_cast(event); - QPointF originalPos = mouseEvent->localPos(); - QPointF transformedPos = _mouseTranslator(originalPos); - transformedPos = mapWindowToUi(transformedPos, originalDestination); - QMouseEvent mappedEvent(mouseEvent->type(), - transformedPos, - mouseEvent->screenPos(), mouseEvent->button(), - mouseEvent->buttons(), mouseEvent->modifiers()); - if (event->type() == QEvent::MouseMove) { - _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos); - } - mappedEvent.ignore(); - if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) { - return mappedEvent.isAccepted(); - } - break; - } - - default: - break; - } - - return false; -} - -void OffscreenUi::lockTexture(int texture) { - _fboCache.lockTexture(texture); -} - -void OffscreenUi::releaseTexture(int texture) { - _fboCache.releaseTexture(texture); -} - -void OffscreenUi::pause() { - _paused = true; -} - -void OffscreenUi::resume() { - _paused = false; - requestRender(); -} - -bool OffscreenUi::isPaused() const { - return _paused; -} - -void OffscreenUi::setProxyWindow(QWindow* window) { - _renderControl->_renderWindow = window; -} void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { - QQuickItem* item = _rootItem->findChild(name); + QQuickItem* item = getRootItem()->findChild(name); // First load? if (!item) { load(url, f); - item = _rootItem->findChild(name); + item = getRootItem()->findChild(name); } if (item) { item->setEnabled(true); @@ -427,11 +75,11 @@ void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { - QQuickItem* item = _rootItem->findChild(name); + QQuickItem* item = getRootItem()->findChild(name); // First load? if (!item) { load(url, f); - item = _rootItem->findChild(name); + item = getRootItem()->findChild(name); } if (item) { item->setEnabled(!item->isEnabled()); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index ce40bec943..d3567bbb5e 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -12,25 +12,10 @@ #ifndef hifi_OffscreenUi_h #define hifi_OffscreenUi_h -#include -#include -#include -#include -#include -#include -#include -#include +#include "OffscreenQmlSurface.h" -#include -#include - -#include -#include #include -#include "OffscreenGlCanvas.h" -#include "FboCache.h" -#include #define HIFI_QML_DECL \ private: \ @@ -96,53 +81,15 @@ private: offscreenUi->load(QML, f); \ } -class OffscreenUi : public OffscreenGlCanvas, public Dependency { +class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT - class QMyQuickRenderControl : public QQuickRenderControl { - protected: - QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ - if (nullptr == _renderWindow) { - return QQuickRenderControl::renderWindow(offset); - } - if (nullptr != offset) { - offset->rx() = offset->ry() = 0; - } - return _renderWindow; - } - - private: - QWindow* _renderWindow{ nullptr }; - friend class OffscreenUi; - }; - public: - using MouseTranslator = std::function; OffscreenUi(); virtual ~OffscreenUi(); - void create(QOpenGLContext* context); - void resize(const QSize& size); - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { - return load(QUrl(qmlSourceFile), f); - } void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); - void setBaseUrl(const QUrl& baseUrl); - void addImportPath(const QString& path); - //QQmlContext* getQmlContext(); - QQuickItem* getRootItem(); - void pause(); - void resume(); - bool isPaused() const; - void setProxyWindow(QWindow* window); bool shouldSwallowShortcut(QEvent* event); - QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); - virtual bool eventFilter(QObject* originalDestination, QEvent* event); - void setMouseTranslator(MouseTranslator mouseTranslator) { - _mouseTranslator = mouseTranslator; - } - // Messagebox replacement functions using ButtonCallback = std::function; @@ -168,33 +115,6 @@ public: static void critical(const QString& title, const QString& text, ButtonCallback callback = NO_OP_CALLBACK, QMessageBox::StandardButtons buttons = QMessageBox::Ok); - -private: - QObject* finishQmlLoad(std::function f); - -private slots: - void updateQuick(); - -public slots: - void requestUpdate(); - void requestRender(); - void lockTexture(int texture); - void releaseTexture(int texture); - -signals: - void textureUpdated(GLuint texture); - -private: - QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; - QQuickWindow* _quickWindow{ nullptr }; - QQmlEngine* _qmlEngine{ nullptr }; - QQmlComponent* _qmlComponent{ nullptr }; - QQuickItem* _rootItem{ nullptr }; - QTimer _updateTimer; - FboCache _fboCache; - bool _polish{ true }; - bool _paused{ true }; - MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; }; #endif