Working on web entities

This commit is contained in:
Brad Davis 2015-05-12 21:15:54 -07:00
parent 8c4f802dbf
commit e9b83e1c02
12 changed files with 655 additions and 477 deletions

View file

@ -665,6 +665,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'));
@ -1033,7 +1035,7 @@
<div class="web-section property">
<div class="label">Source URL</div>
<div class="value">
<input type="text" id="property-source-url" class="url"></input>
<input type="text" id="property-web-source-url" class="url"></input>
</div>
</div>

View file

@ -0,0 +1,45 @@
import QtQuick 2.3
import QtQuick.Controls 1.2
import QtWebKit 3.0
Item {
id: root
implicitHeight: 600
implicitWidth: 800
Rectangle {
anchors.margins: 120
color: "#7f0000ff"
anchors.right: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
anchors.rightMargin: 10
anchors.leftMargin: 10
anchors.bottomMargin: 10
anchors.topMargin: 10
}
Rectangle {
color: "#7Fff0000"
anchors.left: parent.horizontalCenter
anchors.leftMargin: 10
anchors.top: parent.top
anchors.margins: 120
anchors.right: parent.right
anchors.topMargin: 10
anchors.bottomMargin: 10
anchors.bottom: parent.bottom
anchors.rightMargin: 10
/*
ScrollView {
id: scrollView
anchors.fill: parent
WebView {
id: webview
objectName: "webview"
anchors.fill: parent
}
}
*/
}
}

View file

@ -16,16 +16,52 @@
#include <GeometryCache.h>
#include <PerfStat.h>
#include <TextRenderer.h>
#include "GLMHelpers.h"
#include <OffscreenQmlSurface.h>
#include <GLMHelpers.h>
#include <PathUtils.h>
#include <TextureCache.h>
#include <gpu/GLBackend.h>
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) {
}
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();
updateQmlSourceUrl();
QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) {
_webSurface->lockTexture(textureId);
assert(!glGetError());
std::swap(_texture, textureId);
if (textureId) {
_webSurface->releaseTexture(textureId);
}
});
}
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();
@ -42,16 +78,50 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z);
float alpha = 1.0f; //getBackgroundAlpha();
glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, 0);
glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, 0);
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);
}
// TODO: Determine if we want these entities to have the deferred lighting effect? I think we do, so that the color
// used for a sphere, or box have the same look as those used on a text entity.
DependencyManager::get<DeferredLightingEffect>()->bindSimpleProgram();
DependencyManager::get<GeometryCache>()->renderQuad(topLeft, bottomRight, glm::vec4(0, 1, 0, alpha));
DependencyManager::get<DeferredLightingEffect>()->releaseSimpleProgram();
}
DependencyManager::get<GeometryCache>()->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;
// Update the offscreen display
updateQmlSourceUrl();
}
}
void RenderableWebEntityItem::updateQmlSourceUrl() {
if (!_webSurface) {
return;
}
auto webView = _webSurface->getRootItem()->findChild<QQuickItem*>("webview");
if (!webView) {
return;
}
if (!_sourceUrl.isEmpty()) {
webView->setProperty("url", _sourceUrl);
} else {
webView->setProperty("url", QVariant());
}
}

View file

@ -11,15 +11,22 @@
#include <WebEntityItem.h>
class OffscreenQmlSurface;
class RenderableWebEntityItem : public WebEntityItem {
public:
static EntityItem* factory(const EntityItemID& entityID, const EntityItemProperties& properties);
RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) :
WebEntityItem(entityItemID, properties)
{ }
RenderableWebEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties);
virtual void render(RenderArgs* args);
virtual void setSourceUrl(const QString& value);
private:
void updateQmlSourceUrl();
OffscreenQmlSurface* _webSurface{ nullptr };
uint32_t _texture{ 0 };
};

View file

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

View file

@ -140,6 +140,8 @@ 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);

View file

@ -44,7 +44,7 @@ void WebEntityItem::setDimensions(const glm::vec3& value) {
EntityItemProperties WebEntityItem::getProperties() const {
EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class
COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getSource);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl);
return properties;
}
@ -52,7 +52,7 @@ 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(modelURL, setSourceUrl);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(sourceUrl, setSourceUrl);
if (somethingChanged) {
bool wantDebug = false;
@ -143,3 +143,11 @@ bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const g
}
return intersects;
}
void WebEntityItem::setSourceUrl(const QString& value) {
if (_sourceUrl != value) {
_sourceUrl = value;
}
}
const QString& WebEntityItem::getSourceUrl() const { return _sourceUrl; }

View file

@ -49,8 +49,8 @@ public:
bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face,
void** intersectedObject, bool precisionPicking) const;
void setSourceUrl(const QString& value) { _sourceUrl = value; }
const QString& getSourceUrl() const { return _sourceUrl; }
virtual void setSourceUrl(const QString& value);
const QString& getSourceUrl() const;
protected:
QString _sourceUrl;

View file

@ -0,0 +1,372 @@
//
// OffscreenUi.cpp
// interface/src/render-utils
//
// 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
//
#include "OffscreenQmlSurface.h"
#include <QOpenGLFramebufferObject>
#include <QOpenGLDebugLogger>
#include <QGLWidget>
#include <QtQml>
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::WindowFlags>(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) {
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
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<void(QQmlContext*, QObject*)> 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<void(QQmlContext*, QObject*)> f) {
disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0);
if (_qmlComponent->isError()) {
QList<QQmlError> 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<QQmlError> 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<QQuickItem*>(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<QWidget*>(sourceObject)) {
sourceSize = toGlm(((QWidget*)sourceObject)->size());
} else if (dynamic_cast<QWindow*>(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<QResizeEvent*>(event);
QGLWidget* widget = dynamic_cast<QGLWidget*>(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<QWheelEvent*>(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<QMouseEvent*>(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;
}

View file

@ -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 <QQmlEngine>
#include <QQmlComponent>
#include <QQuickItem>
#include <QQuickWindow>
#include <QQuickRenderControl>
#include <QQuickImageProvider>
#include <QTimer>
#include <QMessageBox>
#include <atomic>
#include <functional>
#include <GLMHelpers.h>
#include <ThreadHelpers.h>
#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<QPointF(const QPointF&)>;
void create(QOpenGLContext* context);
void resize(const QSize& size);
QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
QObject* load(const QString& qmlSourceFile, std::function<void(QQmlContext*, QObject*)> 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<void(QQmlContext*, QObject*)> 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

View file

@ -16,252 +16,18 @@
#include "MessageDialog.h"
Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
class OffscreenUiRoot : public QQuickItem {
Q_OBJECT
public:
// 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::WindowFlags>(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) {
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);
OffscreenUiRoot(QQuickItem* parent = 0);
Q_INVOKABLE void information(const QString& title, const QString& text);
Q_INVOKABLE void loadChild(const QUrl& url) {
DependencyManager::get<OffscreenUi>()->load(url);
}
};
// 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<void(QQmlContext*, QObject*)> 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<void(QQmlContext*, QObject*)> f) {
disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0);
if (_qmlComponent->isError()) {
QList<QQmlError> 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<QQmlError> 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<QQuickItem*>(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<QWidget*>(sourceObject)) {
sourceSize = toGlm(((QWidget*)sourceObject)->size());
} else if (dynamic_cast<QWindow*>(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
@ -281,128 +47,6 @@ bool OffscreenUi::shouldSwallowShortcut(QEvent* event) {
return false;
}
///////////////////////////////////////////////////////
//
// 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<QResizeEvent*>(event);
QGLWidget* widget = dynamic_cast<QGLWidget*>(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<QWheelEvent*>(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<QMouseEvent*>(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;
}
class OffscreenUiRoot : public QQuickItem {
Q_OBJECT
public:
OffscreenUiRoot(QQuickItem* parent = 0);
Q_INVOKABLE void information(const QString& title, const QString& text);
Q_INVOKABLE void loadChild(const QUrl& url) {
DependencyManager::get<OffscreenUi>()->load(url);
}
};
OffscreenUiRoot::OffscreenUiRoot(QQuickItem* parent) : QQuickItem(parent) {
}

View file

@ -12,25 +12,10 @@
#ifndef hifi_OffscreenUi_h
#define hifi_OffscreenUi_h
#include <QQmlEngine>
#include <QQmlComponent>
#include <QQuickItem>
#include <QQuickWindow>
#include <QQuickRenderControl>
#include <QQuickImageProvider>
#include <QTimer>
#include <QMessageBox>
#include "OffscreenQmlSurface.h"
#include <atomic>
#include <functional>
#include <GLMHelpers.h>
#include <ThreadHelpers.h>
#include <DependencyManager.h>
#include "OffscreenGlCanvas.h"
#include "FboCache.h"
#include <QQuickItem>
#define HIFI_QML_DECL \
private: \
@ -96,84 +81,6 @@ private:
offscreenUi->load(QML, f); \
}
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<QPointF(const QPointF&)>;
void create(QOpenGLContext* context);
void resize(const QSize& size);
QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
QObject* load(const QString& qmlSourceFile, std::function<void(QQmlContext*, QObject*)> 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<void(QQmlContext*, QObject*)> 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; } };
};
class OffscreenUi : public OffscreenQmlSurface, public Dependency {
Q_OBJECT