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 @@
-
+
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