mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge branch 'master' of https://github.com/highfidelity/hifi into removeLogSpam
Conflicts: libraries/script-engine/src/FileScriptingInterface.cpp
This commit is contained in:
commit
c65471e880
45 changed files with 2139 additions and 513 deletions
|
@ -17,7 +17,10 @@
|
|||
var KEYBOARD_HEIGHT = 200;
|
||||
|
||||
function shouldRaiseKeyboard() {
|
||||
if (document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA") {
|
||||
var nodeName = document.activeElement.nodeName;
|
||||
var nodeType = document.activeElement.type;
|
||||
if (nodeName === "INPUT" && (nodeType === "text" || nodeType === "number" || nodeType === "password")
|
||||
|| document.activeElement.nodeName === "TEXTAREA") {
|
||||
return true;
|
||||
} else {
|
||||
// check for contenteditable attribute
|
||||
|
|
|
@ -23,6 +23,8 @@ ScrollingWindow {
|
|||
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge
|
||||
|
||||
signal loadingChanged(int status)
|
||||
|
||||
x: 100
|
||||
y: 100
|
||||
|
||||
|
@ -44,6 +46,10 @@ ScrollingWindow {
|
|||
hidePermissionsBar();
|
||||
}
|
||||
|
||||
function setAutoAdd(auto) {
|
||||
desktop.setAutoAdd(auto);
|
||||
}
|
||||
|
||||
Item {
|
||||
id:item
|
||||
width: pane.contentWidth
|
||||
|
@ -197,7 +203,7 @@ ScrollingWindow {
|
|||
|
||||
WebView {
|
||||
id: webview
|
||||
url: "https://highfidelity.com"
|
||||
url: "https://highfidelity.com/"
|
||||
|
||||
property alias eventBridgeWrapper: eventBridgeWrapper
|
||||
|
||||
|
@ -243,6 +249,7 @@ ScrollingWindow {
|
|||
if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
|
||||
addressBar.text = loadRequest.url
|
||||
}
|
||||
root.loadingChanged(loadRequest.status);
|
||||
}
|
||||
|
||||
onIconChanged: {
|
||||
|
@ -254,7 +261,7 @@ ScrollingWindow {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
desktop.initWebviewProfileHandlers(webview.profile)
|
||||
desktop.initWebviewProfileHandlers(webview.profile);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
//
|
||||
// Marketplaces.qml
|
||||
//
|
||||
// Created by Elisa Lupin-Jimenez on 3 Aug 2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebChannel 1.0
|
||||
import QtWebEngine 1.1
|
||||
import QtWebSockets 1.0
|
||||
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
|
||||
|
||||
import "controls"
|
||||
import "controls-uit" as Controls
|
||||
import "styles"
|
||||
import "styles-uit"
|
||||
|
||||
|
||||
Rectangle {
|
||||
HifiConstants { id: hifi }
|
||||
id: marketplace
|
||||
anchors.fill: parent
|
||||
property var marketplacesUrl: "../../scripts/system/html/marketplaces.html"
|
||||
property int statusBarHeight: 50
|
||||
property int statusMargin: 50
|
||||
property string standardMessage: "Check out other marketplaces."
|
||||
property string claraMessage: "Choose a model and click Download -> Autodesk FBX."
|
||||
property string claraError: "High Fidelity only supports Autodesk FBX models."
|
||||
|
||||
Controls.BaseWebView {
|
||||
id: webview
|
||||
url: marketplacesUrl
|
||||
anchors.top: marketplace.top
|
||||
width: parent.width
|
||||
height: parent.height - statusBarHeight
|
||||
focus: true
|
||||
|
||||
Timer {
|
||||
id: zipTimer
|
||||
running: false
|
||||
repeat: false
|
||||
interval: 1500
|
||||
property var handler;
|
||||
onTriggered: handler();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: alertTimer
|
||||
running: false
|
||||
repeat: false
|
||||
interval: 9000
|
||||
property var handler;
|
||||
onTriggered: handler();
|
||||
}
|
||||
|
||||
property var autoCancel: 'var element = $("a.btn.cancel");
|
||||
element.click();'
|
||||
|
||||
property var simpleDownload: 'var element = $("a.download-file");
|
||||
element.removeClass("download-file");
|
||||
element.removeAttr("download");'
|
||||
|
||||
function displayErrorStatus() {
|
||||
alertTimer.handler = function() {
|
||||
statusLabel.text = claraMessage;
|
||||
statusBar.color = hifi.colors.blueHighlight;
|
||||
statusIcon.text = hifi.glyphs.info;
|
||||
}
|
||||
alertTimer.start();
|
||||
}
|
||||
|
||||
property var notFbxHandler: 'var element = $("a.btn.btn-primary.viewer-button.download-file")
|
||||
element.click();'
|
||||
|
||||
// this code is for removing other file types from Clara.io's download options
|
||||
//property var checkFileType: "$('[data-extension]:not([data-extension=\"fbx\"])').parent().remove()"
|
||||
|
||||
onLinkHovered: {
|
||||
desktop.currentUrl = hoveredUrl;
|
||||
//runJavaScript(checkFileType, function(){console.log("Remove filetypes JS injection");});
|
||||
if (File.isZippedFbx(desktop.currentUrl)) {
|
||||
runJavaScript(simpleDownload, function(){console.log("Download JS injection");});
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.isZipped(desktop.currentUrl)) {
|
||||
statusLabel.text = claraError;
|
||||
statusBar.color = hifi.colors.redHighlight;
|
||||
statusIcon.text = hifi.glyphs.alert;
|
||||
runJavaScript(notFbxHandler, displayErrorStatus());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onLoadingChanged: {
|
||||
if (File.isClaraLink(webview.url)) {
|
||||
statusLabel.text = claraMessage;
|
||||
} else {
|
||||
statusLabel.text = standardMessage;
|
||||
}
|
||||
statusBar.color = hifi.colors.blueHighlight;
|
||||
statusIcon.text = hifi.glyphs.info;
|
||||
}
|
||||
|
||||
onNewViewRequested: {
|
||||
var component = Qt.createComponent("Browser.qml");
|
||||
var newWindow = component.createObject(desktop);
|
||||
request.openIn(newWindow.webView);
|
||||
if (File.isZippedFbx(desktop.currentUrl)) {
|
||||
runJavaScript(autoCancel);
|
||||
zipTimer.handler = function() {
|
||||
newWindow.destroy();
|
||||
}
|
||||
zipTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: statusBar
|
||||
anchors.top: webview.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: hifi.colors.blueHighlight
|
||||
|
||||
Controls.Button {
|
||||
id: switchMarketView
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: statusMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 150
|
||||
text: "See all markets"
|
||||
onClicked: {
|
||||
webview.url = "../../scripts/system/html/marketplaces.html";
|
||||
statusLabel.text = standardMessage;
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
id: statusLabel
|
||||
anchors.verticalCenter: switchMarketView.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: statusMargin
|
||||
color: hifi.colors.white
|
||||
text: standardMessage
|
||||
size: 18
|
||||
}
|
||||
|
||||
HiFiGlyphs {
|
||||
id: statusIcon
|
||||
anchors.right: statusLabel.left
|
||||
anchors.verticalCenter: statusLabel.verticalCenter
|
||||
text: hifi.glyphs.info
|
||||
color: hifi.colors.white
|
||||
size: hifi.fontSizes.tableHeadingIcon
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -27,6 +27,7 @@ Windows.ScrollingWindow {
|
|||
destroyOnCloseButton: false
|
||||
property alias source: webview.url
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge;
|
||||
property alias scriptUrl: webview.userScriptUrl
|
||||
|
||||
QtObject {
|
||||
id: eventBridgeWrapper
|
||||
|
@ -71,6 +72,8 @@ Windows.ScrollingWindow {
|
|||
focus: true
|
||||
webChannel.registeredObjects: [eventBridgeWrapper]
|
||||
|
||||
property string userScriptUrl: ""
|
||||
|
||||
// Create a global EventBridge object for raiseAndLowerKeyboard.
|
||||
WebEngineScript {
|
||||
id: createGlobalEventBridge
|
||||
|
@ -87,7 +90,25 @@ Windows.ScrollingWindow {
|
|||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
|
||||
// User script.
|
||||
WebEngineScript {
|
||||
id: userScript
|
||||
sourceUrl: webview.userScriptUrl
|
||||
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
|
||||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
|
||||
|
||||
function onWebEventReceived(event) {
|
||||
if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") {
|
||||
ApplicationInterface.addAssetToWorldFromURL(event.slice(18));
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
eventBridge.webEventReceived.connect(onWebEventReceived);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
import QtQuick 2.3
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebChannel 1.0
|
||||
import QtWebEngine 1.1
|
||||
import QtWebEngine 1.2
|
||||
import QtWebSockets 1.0
|
||||
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
|
||||
|
||||
import "windows" as Windows
|
||||
import "controls"
|
||||
|
@ -23,10 +22,22 @@ Windows.Window {
|
|||
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
|
||||
destroyOnCloseButton: false
|
||||
property var source;
|
||||
property var eventBridge;
|
||||
property var component;
|
||||
property var dynamicContent;
|
||||
|
||||
// Keyboard control properties in case needed by QML content.
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
// JavaScript event bridge object in case QML content includes Web content.
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge;
|
||||
|
||||
QtObject {
|
||||
id: eventBridgeWrapper
|
||||
WebChannel.id: "eventBridgeWrapper"
|
||||
property var eventBridge;
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
if (dynamicContent) {
|
||||
|
@ -72,7 +83,6 @@ Windows.Window {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
id: contentHolder
|
||||
anchors.fill: parent
|
||||
|
|
27
interface/resources/qml/Web3DOverlay.qml
Normal file
27
interface/resources/qml/Web3DOverlay.qml
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Web3DOverlay.qml
|
||||
//
|
||||
// Created by David Rowe on 16 Dec 2016.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
|
||||
import "controls" as Controls
|
||||
|
||||
Controls.WebView {
|
||||
|
||||
function onWebEventReceived(event) {
|
||||
if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") {
|
||||
ApplicationInterface.addAssetToWorldFromURL(event.slice(18));
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
eventBridge.webEventReceived.connect(onWebEventReceived);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import HFWebEngineProfile 1.0
|
|||
|
||||
Item {
|
||||
property alias url: root.url
|
||||
property alias scriptURL: root.userScriptUrl
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge
|
||||
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
|
||||
property bool keyboardRaised: false
|
||||
|
@ -38,6 +39,8 @@ Item {
|
|||
storageName: "qmlWebEngine"
|
||||
}
|
||||
|
||||
property string userScriptUrl: ""
|
||||
|
||||
// creates a global EventBridge object.
|
||||
WebEngineScript {
|
||||
id: createGlobalEventBridge
|
||||
|
@ -54,7 +57,15 @@ Item {
|
|||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
|
||||
// User script.
|
||||
WebEngineScript {
|
||||
id: userScript
|
||||
sourceUrl: root.userScriptUrl
|
||||
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
|
||||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
|
||||
|
||||
property string newUrl: ""
|
||||
|
||||
|
|
|
@ -100,21 +100,25 @@ OriginalDesktop.Desktop {
|
|||
// Accept a download through the webview
|
||||
property bool webViewProfileSetup: false
|
||||
property string currentUrl: ""
|
||||
property string downloadUrl: ""
|
||||
property string adaptedPath: ""
|
||||
property string tempDir: ""
|
||||
property bool autoAdd: false
|
||||
|
||||
function initWebviewProfileHandlers(profile) {
|
||||
console.log("The webview url in desktop is: " + currentUrl);
|
||||
downloadUrl = currentUrl;
|
||||
if (webViewProfileSetup) return;
|
||||
webViewProfileSetup = true;
|
||||
|
||||
profile.downloadRequested.connect(function(download){
|
||||
console.log("Download start: " + download.state);
|
||||
adaptedPath = File.convertUrlToPath(currentUrl);
|
||||
adaptedPath = File.convertUrlToPath(downloadUrl);
|
||||
tempDir = File.getTempDir();
|
||||
console.log("Temp dir created: " + tempDir);
|
||||
download.path = tempDir + "/" + adaptedPath;
|
||||
console.log("Path where object should download: " + download.path);
|
||||
console.log("Auto add: " + autoAdd);
|
||||
download.accept();
|
||||
if (download.state === WebEngineDownloadItem.DownloadInterrupted) {
|
||||
console.log("download failed to complete");
|
||||
|
@ -123,13 +127,18 @@ OriginalDesktop.Desktop {
|
|||
|
||||
profile.downloadFinished.connect(function(download){
|
||||
if (download.state === WebEngineDownloadItem.DownloadCompleted) {
|
||||
File.runUnzip(download.path, currentUrl);
|
||||
File.runUnzip(download.path, downloadUrl, autoAdd);
|
||||
} else {
|
||||
console.log("The download was corrupted, state: " + download.state);
|
||||
}
|
||||
autoAdd = false;
|
||||
})
|
||||
}
|
||||
|
||||
function setAutoAdd(auto) {
|
||||
autoAdd = auto;
|
||||
}
|
||||
|
||||
// Create or fetch a toolbar with the given name
|
||||
function getToolbar(name) {
|
||||
var result = toolbars[name];
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
#include <QtMultimedia/QMediaPlayer>
|
||||
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <gl/QOpenGLContextWrapper.h>
|
||||
|
||||
|
@ -54,6 +55,7 @@
|
|||
#include <AnimDebugDraw.h>
|
||||
#include <BuildInfo.h>
|
||||
#include <AssetClient.h>
|
||||
#include <AssetUpload.h>
|
||||
#include <AutoUpdater.h>
|
||||
#include <AudioInjectorManager.h>
|
||||
#include <CursorManager.h>
|
||||
|
@ -79,6 +81,7 @@
|
|||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
#include <LogHandler.h>
|
||||
#include <MainWindow.h>
|
||||
#include <MappingRequest.h>
|
||||
#include <MessagesClient.h>
|
||||
#include <ModelEntityItem.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
|
@ -157,6 +160,7 @@
|
|||
#include "ui/DialogsManager.h"
|
||||
#include "ui/LoginDialog.h"
|
||||
#include "ui/overlays/Cube3DOverlay.h"
|
||||
#include "ui/overlays/Web3DOverlay.h"
|
||||
#include "ui/Snapshot.h"
|
||||
#include "ui/SnapshotAnimated.h"
|
||||
#include "ui/StandAloneJSConsole.h"
|
||||
|
@ -1190,11 +1194,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
|
||||
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
setKeyboardFocusEntity(entityItemID);
|
||||
});
|
||||
|
||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
|
||||
if (entityItemID == _keyboardFocusedItem.get()) {
|
||||
if (entityItemID == _keyboardFocusedEntity.get()) {
|
||||
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
||||
}
|
||||
});
|
||||
|
@ -1204,7 +1209,26 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
||||
});
|
||||
|
||||
// Keyboard focus handling for Web overlays.
|
||||
auto overlays = &(qApp->getOverlays());
|
||||
|
||||
connect(overlays, &Overlays::mousePressOnOverlay, [=](unsigned int overlayID, const PointerEvent& event) {
|
||||
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
||||
setKeyboardFocusOverlay(overlayID);
|
||||
});
|
||||
|
||||
connect(overlays, &Overlays::overlayDeleted, [=](unsigned int overlayID) {
|
||||
if (overlayID == _keyboardFocusedOverlay.get()) {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
}
|
||||
});
|
||||
|
||||
connect(overlays, &Overlays::mousePressOffOverlay, [=]() {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
});
|
||||
|
||||
connect(this, &Application::aboutToQuit, [=]() {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
||||
});
|
||||
|
||||
|
@ -1476,6 +1500,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
// After all of the constructor is completed, then set firstRun to false.
|
||||
firstRun.set(false);
|
||||
}
|
||||
|
||||
// Monitor model assets (e.g., from Clara.io) added to the world that may need resizing.
|
||||
static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000;
|
||||
_addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS);
|
||||
connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize);
|
||||
|
||||
// Auto-update and close adding asset to world info message box.
|
||||
static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000;
|
||||
_addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS);
|
||||
_addAssetToWorldInfoTimer.setSingleShot(true);
|
||||
connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout);
|
||||
static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000;
|
||||
_addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS);
|
||||
_addAssetToWorldErrorTimer.setSingleShot(true);
|
||||
connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout);
|
||||
|
||||
connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose);
|
||||
connect(&domainHandler, &DomainHandler::hostnameChanged, this, &Application::addAssetToWorldMessageClose);
|
||||
}
|
||||
|
||||
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) {
|
||||
|
@ -1865,9 +1907,9 @@ void Application::initializeUi() {
|
|||
rootContext->setContextProperty("AudioStats", DependencyManager::get<AudioClient>()->getStats().data());
|
||||
rootContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().data());
|
||||
rootContext->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
|
||||
FileScriptingInterface* fileDownload = new FileScriptingInterface(engine);
|
||||
rootContext->setContextProperty("File", fileDownload);
|
||||
connect(fileDownload, &FileScriptingInterface::unzipSuccess, this, &Application::showAssetServerWidget);
|
||||
_fileDownload = new FileScriptingInterface(engine);
|
||||
rootContext->setContextProperty("File", _fileDownload);
|
||||
connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip);
|
||||
rootContext->setContextProperty("MyAvatar", getMyAvatar().get());
|
||||
rootContext->setContextProperty("Messages", DependencyManager::get<MessagesClient>().data());
|
||||
rootContext->setContextProperty("Recording", DependencyManager::get<RecordingScriptingInterface>().data());
|
||||
|
@ -2395,26 +2437,49 @@ bool Application::event(QEvent* event) {
|
|||
}
|
||||
|
||||
{
|
||||
if (!_keyboardFocusedItem.get().isInvalidID()) {
|
||||
if (!_keyboardFocusedEntity.get().isInvalidID()) {
|
||||
switch (event->type()) {
|
||||
case QEvent::KeyPress:
|
||||
case QEvent::KeyRelease: {
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedItem.get());
|
||||
if (entity && entity->getEventHandler()) {
|
||||
event->setAccepted(false);
|
||||
QCoreApplication::sendEvent(entity->getEventHandler(), event);
|
||||
if (event->isAccepted()) {
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
return true;
|
||||
case QEvent::KeyPress:
|
||||
case QEvent::KeyRelease: {
|
||||
//auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get());
|
||||
if (entity && entity->getEventHandler()) {
|
||||
event->setAccepted(false);
|
||||
QCoreApplication::sendEvent(entity->getEventHandler(), event);
|
||||
if (event->isAccepted()) {
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
{
|
||||
if (_keyboardFocusedOverlay.get() != UNKNOWN_OVERLAY_ID) {
|
||||
switch (event->type()) {
|
||||
case QEvent::KeyPress:
|
||||
case QEvent::KeyRelease: {
|
||||
// Only Web overlays can have focus.
|
||||
auto overlay =
|
||||
std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(_keyboardFocusedOverlay.get()));
|
||||
if (overlay && overlay->getEventHandler()) {
|
||||
event->setAccepted(false);
|
||||
QCoreApplication::sendEvent(overlay->getEventHandler(), event);
|
||||
if (event->isAccepted()) {
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2974,6 +3039,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
|
|||
|
||||
if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() ||
|
||||
getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y()))) {
|
||||
getOverlays().mouseMoveEvent(&mappedEvent);
|
||||
getEntities()->mouseMoveEvent(&mappedEvent);
|
||||
}
|
||||
_controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts
|
||||
|
@ -2986,7 +3052,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
|
|||
if (_keyboardMouseDevice->isActive()) {
|
||||
_keyboardMouseDevice->mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Application::mousePressEvent(QMouseEvent* event) {
|
||||
|
@ -3008,6 +3073,7 @@ void Application::mousePressEvent(QMouseEvent* event) {
|
|||
event->buttons(), event->modifiers());
|
||||
|
||||
if (!_aboutToQuit) {
|
||||
getOverlays().mousePressEvent(&mappedEvent);
|
||||
getEntities()->mousePressEvent(&mappedEvent);
|
||||
}
|
||||
|
||||
|
@ -3018,7 +3084,6 @@ void Application::mousePressEvent(QMouseEvent* event) {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
if (hasFocus()) {
|
||||
if (_keyboardMouseDevice->isActive()) {
|
||||
_keyboardMouseDevice->mousePressEvent(event);
|
||||
|
@ -3029,7 +3094,6 @@ void Application::mousePressEvent(QMouseEvent* event) {
|
|||
HFActionEvent actionEvent(HFActionEvent::startType(),
|
||||
computePickRay(mappedEvent.x(), mappedEvent.y()));
|
||||
sendEvent(this, &actionEvent);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3054,6 +3118,7 @@ void Application::mouseReleaseEvent(QMouseEvent* event) {
|
|||
event->buttons(), event->modifiers());
|
||||
|
||||
if (!_aboutToQuit) {
|
||||
getOverlays().mouseReleaseEvent(&mappedEvent);
|
||||
getEntities()->mouseReleaseEvent(&mappedEvent);
|
||||
}
|
||||
|
||||
|
@ -3293,20 +3358,30 @@ void Application::idle(float nsecsElapsed) {
|
|||
}
|
||||
|
||||
|
||||
// Drop focus from _keyboardFocusedItem if no keyboard messages for 30 seconds
|
||||
// Update focus highlight for entity or overlay.
|
||||
{
|
||||
if (!_keyboardFocusedItem.get().isInvalidID()) {
|
||||
if (!_keyboardFocusedEntity.get().isInvalidID() || _keyboardFocusedOverlay.get() != UNKNOWN_OVERLAY_ID) {
|
||||
const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus
|
||||
quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress;
|
||||
if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) {
|
||||
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
} else {
|
||||
// update position of highlight overlay
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedItem.get());
|
||||
if (entity && _keyboardFocusHighlight) {
|
||||
_keyboardFocusHighlight->setRotation(entity->getRotation());
|
||||
_keyboardFocusHighlight->setPosition(entity->getPosition());
|
||||
if (!_keyboardFocusedEntity.get().isInvalidID()) {
|
||||
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get());
|
||||
if (entity && _keyboardFocusHighlight) {
|
||||
_keyboardFocusHighlight->setRotation(entity->getRotation());
|
||||
_keyboardFocusHighlight->setPosition(entity->getPosition());
|
||||
}
|
||||
} else {
|
||||
// Only Web overlays can have focus.
|
||||
auto overlay =
|
||||
std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(_keyboardFocusedOverlay.get()));
|
||||
if (overlay && _keyboardFocusHighlight) {
|
||||
_keyboardFocusHighlight->setRotation(overlay->getRotation());
|
||||
_keyboardFocusHighlight->setPosition(overlay->getPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3894,8 +3969,31 @@ void Application::rotationModeChanged() const {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) {
|
||||
// Create focus
|
||||
if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) {
|
||||
_keyboardFocusHighlight = std::make_shared<Cube3DOverlay>();
|
||||
_keyboardFocusHighlight->setAlpha(1.0f);
|
||||
_keyboardFocusHighlight->setBorderSize(1.0f);
|
||||
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });
|
||||
_keyboardFocusHighlight->setIsSolid(false);
|
||||
_keyboardFocusHighlight->setPulseMin(0.5);
|
||||
_keyboardFocusHighlight->setPulseMax(1.0);
|
||||
_keyboardFocusHighlight->setColorPulse(1.0);
|
||||
_keyboardFocusHighlight->setIgnoreRayIntersection(true);
|
||||
_keyboardFocusHighlight->setDrawInFront(false);
|
||||
_keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight);
|
||||
}
|
||||
|
||||
// Position focus
|
||||
_keyboardFocusHighlight->setRotation(rotation);
|
||||
_keyboardFocusHighlight->setPosition(position);
|
||||
_keyboardFocusHighlight->setDimensions(dimensions);
|
||||
_keyboardFocusHighlight->setVisible(true);
|
||||
}
|
||||
|
||||
QUuid Application::getKeyboardFocusEntity() const {
|
||||
return _keyboardFocusedItem.get();
|
||||
return _keyboardFocusedEntity.get();
|
||||
}
|
||||
|
||||
void Application::setKeyboardFocusEntity(QUuid id) {
|
||||
|
@ -3903,21 +4001,21 @@ void Application::setKeyboardFocusEntity(QUuid id) {
|
|||
setKeyboardFocusEntity(entityItemID);
|
||||
}
|
||||
|
||||
static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f;
|
||||
|
||||
void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
if (_keyboardFocusedItem.get() != entityItemID) {
|
||||
// reset focused entity
|
||||
_keyboardFocusedItem.set(UNKNOWN_ENTITY_ID);
|
||||
if (_keyboardFocusHighlight) {
|
||||
if (_keyboardFocusedEntity.get() != entityItemID) {
|
||||
_keyboardFocusedEntity.set(entityItemID);
|
||||
|
||||
if (_keyboardFocusHighlight && _keyboardFocusedOverlay.get() == UNKNOWN_OVERLAY_ID) {
|
||||
_keyboardFocusHighlight->setVisible(false);
|
||||
}
|
||||
|
||||
// if invalid, return without expensive (locking) operations
|
||||
if (entityItemID == UNKNOWN_ENTITY_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if valid, query properties
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto properties = entityScriptingInterface->getEntityProperties(entityItemID);
|
||||
if (!properties.getLocked() && properties.getVisible()) {
|
||||
auto entity = getEntities()->getTree()->findEntityByID(entityItemID);
|
||||
|
@ -3926,34 +4024,49 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
|
|||
if (_keyboardMouseDevice->isActive()) {
|
||||
_keyboardMouseDevice->pluginFocusOutEvent();
|
||||
}
|
||||
_keyboardFocusedItem.set(entityItemID);
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
|
||||
// create a focus
|
||||
if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) {
|
||||
_keyboardFocusHighlight = std::make_shared<Cube3DOverlay>();
|
||||
_keyboardFocusHighlight->setAlpha(1.0f);
|
||||
_keyboardFocusHighlight->setBorderSize(1.0f);
|
||||
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });
|
||||
_keyboardFocusHighlight->setIsSolid(false);
|
||||
_keyboardFocusHighlight->setPulseMin(0.5);
|
||||
_keyboardFocusHighlight->setPulseMax(1.0);
|
||||
_keyboardFocusHighlight->setColorPulse(1.0);
|
||||
_keyboardFocusHighlight->setIgnoreRayIntersection(true);
|
||||
_keyboardFocusHighlight->setDrawInFront(false);
|
||||
_keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight);
|
||||
}
|
||||
|
||||
// position the focus
|
||||
_keyboardFocusHighlight->setRotation(entity->getRotation());
|
||||
_keyboardFocusHighlight->setPosition(entity->getPosition());
|
||||
_keyboardFocusHighlight->setDimensions(entity->getDimensions() * 1.05f);
|
||||
_keyboardFocusHighlight->setVisible(true);
|
||||
setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(),
|
||||
entity->getDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int Application::getKeyboardFocusOverlay() {
|
||||
return _keyboardFocusedOverlay.get();
|
||||
}
|
||||
|
||||
void Application::setKeyboardFocusOverlay(unsigned int overlayID) {
|
||||
if (overlayID != _keyboardFocusedOverlay.get()) {
|
||||
_keyboardFocusedOverlay.set(overlayID);
|
||||
|
||||
if (_keyboardFocusHighlight && _keyboardFocusedEntity.get() == UNKNOWN_ENTITY_ID) {
|
||||
_keyboardFocusHighlight->setVisible(false);
|
||||
}
|
||||
|
||||
if (overlayID == UNKNOWN_OVERLAY_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto overlayType = getOverlays().getOverlayType(overlayID);
|
||||
auto isVisible = getOverlays().getProperty(overlayID, "visible").value.toBool();
|
||||
if (overlayType == Web3DOverlay::TYPE && isVisible) {
|
||||
auto overlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(overlayID));
|
||||
overlay->setProxyWindow(_window->windowHandle());
|
||||
|
||||
if (_keyboardMouseDevice->isActive()) {
|
||||
_keyboardMouseDevice->pluginFocusOutEvent();
|
||||
}
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
|
||||
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
|
||||
const float OVERLAY_DEPTH = 0.0105f;
|
||||
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::updateDialogs(float deltaTime) const {
|
||||
PerformanceTimer perfTimer("updateDialogs");
|
||||
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
|
||||
|
@ -5515,6 +5628,464 @@ void Application::showAssetServerWidget(QString filePath) {
|
|||
startUpload(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldFromURL(QString url) {
|
||||
qInfo(interfaceapp) << "Download asset and add to world from" << url;
|
||||
|
||||
QString filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL.
|
||||
|
||||
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
|
||||
QString errorInfo = "You do not have permissions to write to the Asset Server.";
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filename, errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
addAssetToWorldInfo(filename, "Downloading asset file " + filename + ".");
|
||||
|
||||
auto request = ResourceManager::createResourceRequest(nullptr, QUrl(url));
|
||||
connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished);
|
||||
request->send();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldFromURLRequestFinished() {
|
||||
auto request = qobject_cast<ResourceRequest*>(sender());
|
||||
auto url = request->getUrl().toString();
|
||||
auto result = request->getResult();
|
||||
|
||||
QString filename = url.section("filename=", 1, 1); // Filename from trailing "?filename=" URL parameter.
|
||||
|
||||
if (result == ResourceRequest::Success) {
|
||||
qInfo(interfaceapp) << "Downloaded asset from" << url;
|
||||
QTemporaryDir temporaryDir;
|
||||
temporaryDir.setAutoRemove(false);
|
||||
if (temporaryDir.isValid()) {
|
||||
QString temporaryDirPath = temporaryDir.path();
|
||||
QString downloadPath = temporaryDirPath + "/" + filename;
|
||||
qInfo(interfaceapp) << "Download path:" << downloadPath;
|
||||
|
||||
QFile tempFile(downloadPath);
|
||||
if (tempFile.open(QIODevice::WriteOnly)) {
|
||||
tempFile.write(request->getData());
|
||||
addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key.
|
||||
qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true);
|
||||
} else {
|
||||
QString errorInfo = "Couldn't open temporary file for download";
|
||||
qWarning(interfaceapp) << errorInfo;
|
||||
addAssetToWorldError(filename, errorInfo);
|
||||
}
|
||||
} else {
|
||||
QString errorInfo = "Couldn't create temporary directory for download";
|
||||
qWarning(interfaceapp) << errorInfo;
|
||||
addAssetToWorldError(filename, errorInfo);
|
||||
}
|
||||
} else {
|
||||
qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString();
|
||||
addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString());
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
}
|
||||
|
||||
|
||||
QString filenameFromPath(QString filePath) {
|
||||
return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1);
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldUnzipFailure(QString filePath) {
|
||||
QString filename = filenameFromPath(QUrl(filePath).toLocalFile());
|
||||
qWarning(interfaceapp) << "Couldn't unzip file" << filePath;
|
||||
addAssetToWorldError(filename, "Couldn't unzip file " + filename + ".");
|
||||
}
|
||||
|
||||
void Application::addAssetToWorld(QString filePath) {
|
||||
// Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget().
|
||||
|
||||
QString path = QUrl(filePath).toLocalFile();
|
||||
QString filename = filenameFromPath(path);
|
||||
QString mapping = "/" + filename;
|
||||
|
||||
// Test repeated because possibly different code paths.
|
||||
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
|
||||
QString errorInfo = "You do not have permissions to write to the Asset Server.";
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filename, errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server.");
|
||||
|
||||
addAssetToWorldWithNewMapping(path, mapping, 0);
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy) {
|
||||
auto request = DependencyManager::get<AssetClient>()->createGetMappingRequest(mapping);
|
||||
|
||||
QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable {
|
||||
const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard.
|
||||
auto result = request->getError();
|
||||
if (result == GetMappingRequest::NotFound) {
|
||||
addAssetToWorldUpload(filePath, mapping);
|
||||
} else if (result != GetMappingRequest::NoError) {
|
||||
QString errorInfo = "Could not map asset name: "
|
||||
+ mapping.left(mapping.length() - QString::number(copy).length() - 1);
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
} else if (copy < MAX_COPY_COUNT - 1) {
|
||||
if (copy > 0) {
|
||||
mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1);
|
||||
}
|
||||
copy++;
|
||||
mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy));
|
||||
addAssetToWorldWithNewMapping(filePath, mapping, copy);
|
||||
} else {
|
||||
QString errorInfo = "Too many copies of asset name: "
|
||||
+ mapping.left(mapping.length() - QString::number(copy).length() - 1);
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
}
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldUpload(QString filePath, QString mapping) {
|
||||
qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping;
|
||||
auto upload = DependencyManager::get<AssetClient>()->createUpload(filePath);
|
||||
QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable {
|
||||
if (upload->getError() != AssetUpload::NoError) {
|
||||
QString errorInfo = "Could not upload asset to the Asset Server.";
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
} else {
|
||||
addAssetToWorldSetMapping(filePath, mapping, hash);
|
||||
}
|
||||
|
||||
// Remove temporary directory created by Clara.io market place download.
|
||||
int index = filePath.lastIndexOf("/model_repo/");
|
||||
if (index > 0) {
|
||||
QString tempDir = filePath.left(index);
|
||||
qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir;
|
||||
QDir(tempDir).removeRecursively();
|
||||
}
|
||||
|
||||
upload->deleteLater();
|
||||
});
|
||||
|
||||
upload->start();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash) {
|
||||
auto request = DependencyManager::get<AssetClient>()->createSetMappingRequest(mapping, hash);
|
||||
connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable {
|
||||
if (request->getError() != SetMappingRequest::NoError) {
|
||||
QString errorInfo = "Could not set asset mapping.";
|
||||
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
} else {
|
||||
addAssetToWorldAddEntity(filePath, mapping);
|
||||
}
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) {
|
||||
EntityItemProperties properties;
|
||||
properties.setType(EntityTypes::Model);
|
||||
properties.setName(mapping.right(mapping.length() - 1));
|
||||
properties.setModelURL("atp:" + mapping);
|
||||
properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND);
|
||||
properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar.
|
||||
properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions.
|
||||
properties.setPosition(getMyAvatar()->getPosition() + getMyAvatar()->getOrientation() * glm::vec3(0.0f, 0.0f, -2.0f));
|
||||
properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f));
|
||||
auto entityID = DependencyManager::get<EntityScriptingInterface>()->addEntity(properties);
|
||||
|
||||
// Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later
|
||||
// on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted.
|
||||
|
||||
if (entityID == QUuid()) {
|
||||
QString errorInfo = "Could not add asset " + mapping + " to world.";
|
||||
qWarning(interfaceapp) << "Could not add asset to world: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
} else {
|
||||
// Monitor when asset is rendered in world so that can resize if necessary.
|
||||
_addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed.
|
||||
if (!_addAssetToWorldResizeTimer.isActive()) {
|
||||
_addAssetToWorldResizeTimer.start();
|
||||
}
|
||||
|
||||
// Close progress message box.
|
||||
addAssetToWorldInfoDone(filenameFromPath(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldCheckModelSize() {
|
||||
if (_addAssetToWorldResizeList.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto item = _addAssetToWorldResizeList.begin();
|
||||
while (item != _addAssetToWorldResizeList.end()) {
|
||||
auto entityID = item.key();
|
||||
|
||||
EntityPropertyFlags propertyFlags;
|
||||
propertyFlags += PROP_NAME;
|
||||
propertyFlags += PROP_DIMENSIONS;
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags);
|
||||
auto name = properties.getName();
|
||||
auto dimensions = properties.getDimensions();
|
||||
|
||||
const QString GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":true}}";
|
||||
bool doResize = false;
|
||||
|
||||
const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f);
|
||||
if (dimensions != DEFAULT_DIMENSIONS) {
|
||||
|
||||
// Scale model so that its maximum is exactly specific size.
|
||||
const float MAXIMUM_DIMENSION = 1.0f;
|
||||
auto previousDimensions = dimensions;
|
||||
auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y,
|
||||
MAXIMUM_DIMENSION / dimensions.z));
|
||||
dimensions *= scale;
|
||||
qInfo(interfaceapp) << "Asset" << name << "auto-resized from" << previousDimensions << " to " << dimensions;
|
||||
doResize = true;
|
||||
|
||||
item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next.
|
||||
} else {
|
||||
// Increment count of checks done.
|
||||
_addAssetToWorldResizeList[entityID]++;
|
||||
|
||||
const int CHECK_MODEL_SIZE_MAX_CHECKS = 300;
|
||||
if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) {
|
||||
// Have done enough checks; model was either the default size or something's gone wrong.
|
||||
|
||||
// Rescale all dimensions.
|
||||
const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f);
|
||||
dimensions = UNIT_DIMENSIONS;
|
||||
qInfo(interfaceapp) << "Asset" << name << "auto-resize timed out; resized to " << dimensions;
|
||||
doResize = true;
|
||||
|
||||
item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next.
|
||||
} else {
|
||||
// No action on this entity; advance to next.
|
||||
++item;
|
||||
}
|
||||
}
|
||||
|
||||
if (doResize) {
|
||||
EntityItemProperties properties;
|
||||
properties.setDimensions(dimensions);
|
||||
properties.setVisible(true);
|
||||
properties.setCollisionless(false);
|
||||
properties.setUserData(GRABBABLE_USER_DATA);
|
||||
properties.setLastEdited(usecTimestampNow());
|
||||
entityScriptingInterface->editEntity(entityID, properties);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer if nothing in list to check.
|
||||
if (_addAssetToWorldResizeList.size() == 0) {
|
||||
_addAssetToWorldResizeTimer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Application::addAssetToWorldInfo(QString modelName, QString infoText) {
|
||||
// Displays the most recent info message, subject to being overridden by error messages.
|
||||
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Cancel info timer if running.
|
||||
If list has an entry for modelName, delete it (just one).
|
||||
Append modelName, infoText to list.
|
||||
Display infoText in message box unless an error is being displayed (i.e., error timer is running).
|
||||
Show message box if not already visible.
|
||||
*/
|
||||
|
||||
_addAssetToWorldInfoTimer.stop();
|
||||
|
||||
addAssetToWorldInfoClear(modelName);
|
||||
|
||||
_addAssetToWorldInfoKeys.append(modelName);
|
||||
_addAssetToWorldInfoMessages.append(infoText);
|
||||
|
||||
if (!_addAssetToWorldErrorTimer.isActive()) {
|
||||
if (!_addAssetToWorldMessageBox) {
|
||||
_addAssetToWorldMessageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION,
|
||||
"Downloading Asset", "", QMessageBox::NoButton, QMessageBox::NoButton);
|
||||
connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed()));
|
||||
}
|
||||
|
||||
_addAssetToWorldMessageBox->setProperty("text", "\n" + infoText);
|
||||
_addAssetToWorldMessageBox->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldInfoClear(QString modelName) {
|
||||
// Clears modelName entry from message list without affecting message currently displayed.
|
||||
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Delete entry for modelName from list.
|
||||
*/
|
||||
|
||||
auto index = _addAssetToWorldInfoKeys.indexOf(modelName);
|
||||
if (index > -1) {
|
||||
_addAssetToWorldInfoKeys.removeAt(index);
|
||||
_addAssetToWorldInfoMessages.removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldInfoDone(QString modelName) {
|
||||
// Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest.
|
||||
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Delete entry for modelName from list.
|
||||
(Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout()
|
||||
*/
|
||||
|
||||
addAssetToWorldInfoClear(modelName);
|
||||
_addAssetToWorldInfoTimer.start();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldInfoTimeout() {
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If list not empty, display last message in list (may already be displayed ) unless an error is being displayed.
|
||||
If list empty, close the message box unless an error is being displayed.
|
||||
*/
|
||||
|
||||
if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) {
|
||||
if (_addAssetToWorldInfoKeys.length() > 0) {
|
||||
_addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last());
|
||||
} else {
|
||||
disconnect(_addAssetToWorldMessageBox);
|
||||
_addAssetToWorldMessageBox->setVisible(false);
|
||||
_addAssetToWorldMessageBox->deleteLater();
|
||||
_addAssetToWorldMessageBox = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Application::addAssetToWorldError(QString modelName, QString errorText) {
|
||||
// Displays the most recent error message for a few seconds.
|
||||
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If list has an entry for modelName, delete it.
|
||||
Display errorText in message box.
|
||||
Show message box if not already visible.
|
||||
(Re)start error timer. ... onAddAssetToWorldErrorTimeout()
|
||||
*/
|
||||
|
||||
addAssetToWorldInfoClear(modelName);
|
||||
|
||||
if (!_addAssetToWorldMessageBox) {
|
||||
_addAssetToWorldMessageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION,
|
||||
"Downloading Asset", "", QMessageBox::NoButton, QMessageBox::NoButton);
|
||||
connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed()));
|
||||
}
|
||||
|
||||
_addAssetToWorldMessageBox->setProperty("text", "\n" + errorText);
|
||||
_addAssetToWorldMessageBox->setVisible(true);
|
||||
|
||||
_addAssetToWorldErrorTimer.start();
|
||||
}
|
||||
|
||||
void Application::addAssetToWorldErrorTimeout() {
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
If list is not empty, display message from last entry.
|
||||
If list is empty, close the message box.
|
||||
*/
|
||||
|
||||
if (_addAssetToWorldMessageBox) {
|
||||
if (_addAssetToWorldInfoKeys.length() > 0) {
|
||||
_addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last());
|
||||
} else {
|
||||
disconnect(_addAssetToWorldMessageBox);
|
||||
_addAssetToWorldMessageBox->setVisible(false);
|
||||
_addAssetToWorldMessageBox->deleteLater();
|
||||
_addAssetToWorldMessageBox = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Application::addAssetToWorldMessageClose() {
|
||||
// Clear messages, e.g., if Interface is being closed or domain changes.
|
||||
|
||||
/*
|
||||
Call if user manually closes message box.
|
||||
Call if domain changes.
|
||||
Call if application is shutting down.
|
||||
|
||||
Stop timers.
|
||||
Close the message box if open.
|
||||
Clear lists.
|
||||
*/
|
||||
|
||||
_addAssetToWorldInfoTimer.stop();
|
||||
_addAssetToWorldErrorTimer.stop();
|
||||
|
||||
if (_addAssetToWorldMessageBox) {
|
||||
disconnect(_addAssetToWorldMessageBox);
|
||||
_addAssetToWorldMessageBox->setVisible(false);
|
||||
_addAssetToWorldMessageBox->deleteLater();
|
||||
_addAssetToWorldMessageBox = nullptr;
|
||||
}
|
||||
|
||||
_addAssetToWorldInfoKeys.clear();
|
||||
_addAssetToWorldInfoMessages.clear();
|
||||
}
|
||||
|
||||
void Application::onAssetToWorldMessageBoxClosed() {
|
||||
if (_addAssetToWorldMessageBox) {
|
||||
// User manually closed message box; perhaps because it has become stuck, so reset all messages.
|
||||
qInfo(interfaceapp) << "User manually closed download status message box";
|
||||
disconnect(_addAssetToWorldMessageBox);
|
||||
_addAssetToWorldMessageBox = nullptr;
|
||||
addAssetToWorldMessageClose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Application::handleUnzip(QString zipFile, QString unzipFile, bool autoAdd) {
|
||||
if (autoAdd) {
|
||||
if (!unzipFile.isEmpty()) {
|
||||
addAssetToWorld(unzipFile);
|
||||
} else {
|
||||
addAssetToWorldUnzipFailure(zipFile);
|
||||
}
|
||||
} else {
|
||||
showAssetServerWidget(unzipFile);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::packageModel() {
|
||||
ModelPackager::package();
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <QtCore/QPointer>
|
||||
#include <QtCore/QSet>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtQuick/QQuickItem>
|
||||
|
||||
#include <QtGui/QImage>
|
||||
|
||||
|
@ -28,6 +29,7 @@
|
|||
#include <AbstractViewStateInterface.h>
|
||||
#include <EntityEditPacketSender.h>
|
||||
#include <EntityTreeRenderer.h>
|
||||
#include <FileScriptingInterface.h>
|
||||
#include <input-plugins/KeyboardMouseDevice.h>
|
||||
#include <input-plugins/TouchscreenDevice.h>
|
||||
#include <OctreeQuery.h>
|
||||
|
@ -310,6 +312,20 @@ public slots:
|
|||
void toggleRunningScriptsWidget() const;
|
||||
Q_INVOKABLE void showAssetServerWidget(QString filePath = "");
|
||||
|
||||
// FIXME: Move addAssetToWorld* methods to own class?
|
||||
void addAssetToWorldFromURL(QString url);
|
||||
void addAssetToWorldFromURLRequestFinished();
|
||||
void addAssetToWorld(QString filePath);
|
||||
void addAssetToWorldUnzipFailure(QString filePath);
|
||||
void addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy);
|
||||
void addAssetToWorldUpload(QString filePath, QString mapping);
|
||||
void addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash);
|
||||
void addAssetToWorldAddEntity(QString filePath, QString mapping);
|
||||
|
||||
void handleUnzip(QString sourceFile, QString destinationFile, bool autoAdd);
|
||||
|
||||
FileScriptingInterface* getFileDownloadInterface() { return _fileDownload; }
|
||||
|
||||
void handleLocalServerConnection() const;
|
||||
void readArgumentsFromLocalSocket() const;
|
||||
|
||||
|
@ -352,10 +368,17 @@ public slots:
|
|||
|
||||
static void runTests();
|
||||
|
||||
void setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions);
|
||||
|
||||
QUuid getKeyboardFocusEntity() const; // thread-safe
|
||||
void setKeyboardFocusEntity(QUuid id);
|
||||
void setKeyboardFocusEntity(EntityItemID entityItemID);
|
||||
|
||||
unsigned int getKeyboardFocusOverlay();
|
||||
void setKeyboardFocusOverlay(unsigned int overlayID);
|
||||
|
||||
void addAssetToWorldMessageClose();
|
||||
|
||||
private slots:
|
||||
void showDesktop();
|
||||
void clearDomainOctreeDetails();
|
||||
|
@ -392,6 +415,12 @@ private slots:
|
|||
void updateDisplayMode();
|
||||
void domainConnectionRefused(const QString& reasonMessage, int reason, const QString& extraInfo);
|
||||
|
||||
void addAssetToWorldCheckModelSize();
|
||||
|
||||
void onAssetToWorldMessageBoxClosed();
|
||||
void addAssetToWorldInfoTimeout();
|
||||
void addAssetToWorldErrorTimeout();
|
||||
|
||||
private:
|
||||
static void initDisplay();
|
||||
void init();
|
||||
|
@ -567,7 +596,8 @@ private:
|
|||
|
||||
DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface = new DialogsManagerScriptingInterface();
|
||||
|
||||
ThreadSafeValueCache<EntityItemID> _keyboardFocusedItem;
|
||||
ThreadSafeValueCache<EntityItemID> _keyboardFocusedEntity;
|
||||
ThreadSafeValueCache<unsigned int> _keyboardFocusedOverlay;
|
||||
quint64 _lastAcceptedKeyPress = 0;
|
||||
bool _isForeground = true; // starts out assumed to be in foreground
|
||||
bool _inPaint = false;
|
||||
|
@ -609,6 +639,22 @@ private:
|
|||
model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ;
|
||||
gpu::TexturePointer _defaultSkyboxTexture;
|
||||
gpu::TexturePointer _defaultSkyboxAmbientTexture;
|
||||
|
||||
QTimer _addAssetToWorldResizeTimer;
|
||||
QHash<QUuid, int> _addAssetToWorldResizeList;
|
||||
|
||||
void addAssetToWorldInfo(QString modelName, QString infoText);
|
||||
void addAssetToWorldInfoClear(QString modelName);
|
||||
void addAssetToWorldInfoDone(QString modelName);
|
||||
void addAssetToWorldError(QString modelName, QString errorText);
|
||||
|
||||
QQuickItem* _addAssetToWorldMessageBox{ nullptr };
|
||||
QStringList _addAssetToWorldInfoKeys; // Model name
|
||||
QStringList _addAssetToWorldInfoMessages; // Info message
|
||||
QTimer _addAssetToWorldInfoTimer;
|
||||
QTimer _addAssetToWorldErrorTimer;
|
||||
|
||||
FileScriptingInterface* _fileDownload;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -202,9 +202,8 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec
|
|||
if (_texture && _texture->isLoaded()) {
|
||||
// Make sure position and rotation is updated.
|
||||
Transform transform = getTransform();
|
||||
// XXX this code runs too often for this...
|
||||
// applyTransformTo(transform, true);
|
||||
// setTransform(transform);
|
||||
|
||||
// Don't call applyTransformTo() or setTransform() here because this code runs too frequently.
|
||||
|
||||
// Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale.
|
||||
bool isNull = _fromImage.isNull();
|
||||
|
|
|
@ -81,6 +81,7 @@ void ModelOverlay::render(RenderArgs* args) {
|
|||
}
|
||||
|
||||
_model->setVisibleInScene(_visible, scene);
|
||||
_model->setLayeredInFront(getDrawInFront(), scene);
|
||||
|
||||
scene->enqueuePendingChanges(pendingChanges);
|
||||
}
|
||||
|
|
|
@ -294,6 +294,14 @@ QString Overlays::getOverlayType(unsigned int overlayId) const {
|
|||
return "";
|
||||
}
|
||||
|
||||
QObject* Overlays::getOverlayObject(unsigned int id) {
|
||||
Overlay::Pointer thisOverlay = getOverlay(id);
|
||||
if (thisOverlay) {
|
||||
return qobject_cast<QObject*>(&(*thisOverlay));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
unsigned int Overlays::getParentPanel(unsigned int childId) const {
|
||||
Overlay::Pointer overlay = getOverlay(childId);
|
||||
auto attachable = std::dynamic_pointer_cast<PanelAttachable>(overlay);
|
||||
|
@ -390,7 +398,7 @@ QScriptValue OverlayPropertyResultToScriptValue(QScriptEngine* engine, const Ove
|
|||
if (!value.value.isValid()) {
|
||||
return QScriptValue::UndefinedValue;
|
||||
}
|
||||
return engine->newVariant(value.value);
|
||||
return engine->toScriptValue(value.value);
|
||||
}
|
||||
|
||||
void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPropertyResult& value) {
|
||||
|
@ -598,6 +606,38 @@ bool Overlays::isAddedOverlay(unsigned int id) {
|
|||
return _overlaysHUD.contains(id) || _overlaysWorld.contains(id);
|
||||
}
|
||||
|
||||
void Overlays::sendMousePressOnOverlay(unsigned int overlayID, const PointerEvent& event) {
|
||||
emit mousePressOnOverlay(overlayID, event);
|
||||
}
|
||||
|
||||
void Overlays::sendMouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event) {
|
||||
emit mouseReleaseOnOverlay(overlayID, event);
|
||||
}
|
||||
|
||||
void Overlays::sendMouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event) {
|
||||
emit mouseMoveOnOverlay(overlayID, event);
|
||||
}
|
||||
|
||||
void Overlays::sendHoverEnterOverlay(unsigned int id, PointerEvent event) {
|
||||
emit hoverEnterOverlay(id, event);
|
||||
}
|
||||
|
||||
void Overlays::sendHoverOverOverlay(unsigned int id, PointerEvent event) {
|
||||
emit hoverOverOverlay(id, event);
|
||||
}
|
||||
|
||||
void Overlays::sendHoverLeaveOverlay(unsigned int id, PointerEvent event) {
|
||||
emit hoverLeaveOverlay(id, event);
|
||||
}
|
||||
|
||||
unsigned int Overlays::getKeyboardFocusOverlay() const {
|
||||
return qApp->getKeyboardFocusOverlay();
|
||||
}
|
||||
|
||||
void Overlays::setKeyboardFocusOverlay(unsigned int id) {
|
||||
qApp->setKeyboardFocusOverlay(id);
|
||||
}
|
||||
|
||||
float Overlays::width() const {
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
return offscreenUi->getWindow()->size().width();
|
||||
|
@ -607,3 +647,152 @@ float Overlays::height() const {
|
|||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
return offscreenUi->getWindow()->size().height();
|
||||
}
|
||||
|
||||
static const uint32_t MOUSE_POINTER_ID = 0;
|
||||
|
||||
static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay,
|
||||
const RayToOverlayIntersectionResult& rayPickResult) {
|
||||
|
||||
// Project the intersection point onto the local xy plane of the overlay.
|
||||
float distance;
|
||||
glm::vec3 planePosition = position;
|
||||
glm::vec3 planeNormal = rotation * Vectors::UNIT_Z;
|
||||
glm::vec3 overlayDimensions = glm::vec3(dimensions.x, dimensions.y, 0.0f);
|
||||
glm::vec3 rayDirection = pickRay.direction;
|
||||
glm::vec3 rayStart = pickRay.origin;
|
||||
glm::vec3 p;
|
||||
if (rayPlaneIntersection(planePosition, planeNormal, rayStart, rayDirection, distance)) {
|
||||
p = rayStart + rayDirection * distance;
|
||||
} else {
|
||||
p = rayPickResult.intersection;
|
||||
}
|
||||
glm::vec3 localP = glm::inverse(rotation) * (p - position);
|
||||
glm::vec3 normalizedP = (localP / overlayDimensions) + glm::vec3(0.5f);
|
||||
return glm::vec2(normalizedP.x * overlayDimensions.x,
|
||||
(1.0f - normalizedP.y) * overlayDimensions.y); // flip y-axis
|
||||
}
|
||||
|
||||
static uint32_t toPointerButtons(const QMouseEvent& event) {
|
||||
uint32_t buttons = 0;
|
||||
buttons |= event.buttons().testFlag(Qt::LeftButton) ? PointerEvent::PrimaryButton : 0;
|
||||
buttons |= event.buttons().testFlag(Qt::RightButton) ? PointerEvent::SecondaryButton : 0;
|
||||
buttons |= event.buttons().testFlag(Qt::MiddleButton) ? PointerEvent::TertiaryButton : 0;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
static PointerEvent::Button toPointerButton(const QMouseEvent& event) {
|
||||
switch (event.button()) {
|
||||
case Qt::LeftButton:
|
||||
return PointerEvent::PrimaryButton;
|
||||
case Qt::RightButton:
|
||||
return PointerEvent::SecondaryButton;
|
||||
case Qt::MiddleButton:
|
||||
return PointerEvent::TertiaryButton;
|
||||
default:
|
||||
return PointerEvent::NoButtons;
|
||||
}
|
||||
}
|
||||
|
||||
PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray,
|
||||
RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType) {
|
||||
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(overlay);
|
||||
|
||||
QReadLocker lock(&_lock);
|
||||
auto position = thisOverlay->getPosition();
|
||||
auto rotation = thisOverlay->getRotation();
|
||||
auto dimensions = thisOverlay->getSize();
|
||||
|
||||
glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult);
|
||||
PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID,
|
||||
pos2D, rayPickResult.intersection,
|
||||
rayPickResult.surfaceNormal, ray.direction,
|
||||
toPointerButton(*event), toPointerButtons(*event));
|
||||
|
||||
return pointerEvent;
|
||||
}
|
||||
|
||||
void Overlays::mousePressEvent(QMouseEvent* event) {
|
||||
PerformanceTimer perfTimer("Overlays::mousePressEvent");
|
||||
|
||||
PickRay ray = qApp->computePickRay(event->x(), event->y());
|
||||
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
|
||||
if (rayPickResult.intersects) {
|
||||
_currentClickingOnOverlayID = rayPickResult.overlayID;
|
||||
|
||||
// Only Web overlays can have focus.
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentClickingOnOverlayID));
|
||||
if (thisOverlay) {
|
||||
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press);
|
||||
emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent);
|
||||
} else {
|
||||
emit mousePressOffOverlay();
|
||||
}
|
||||
} else {
|
||||
emit mousePressOffOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void Overlays::mouseReleaseEvent(QMouseEvent* event) {
|
||||
PerformanceTimer perfTimer("Overlays::mouseReleaseEvent");
|
||||
|
||||
PickRay ray = qApp->computePickRay(event->x(), event->y());
|
||||
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
|
||||
if (rayPickResult.intersects) {
|
||||
|
||||
// Only Web overlays can have focus.
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(rayPickResult.overlayID));
|
||||
if (thisOverlay) {
|
||||
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Release);
|
||||
emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
_currentClickingOnOverlayID = UNKNOWN_OVERLAY_ID;
|
||||
}
|
||||
|
||||
void Overlays::mouseMoveEvent(QMouseEvent* event) {
|
||||
PerformanceTimer perfTimer("Overlays::mouseMoveEvent");
|
||||
|
||||
PickRay ray = qApp->computePickRay(event->x(), event->y());
|
||||
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
|
||||
if (rayPickResult.intersects) {
|
||||
|
||||
// Only Web overlays can have focus.
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(rayPickResult.overlayID));
|
||||
if (thisOverlay) {
|
||||
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
|
||||
emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent);
|
||||
|
||||
// If previously hovering over a different overlay then leave hover on that overlay.
|
||||
if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) {
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentHoverOverOverlayID));
|
||||
if (thisOverlay) {
|
||||
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
|
||||
emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// If hovering over a new overlay then enter hover on that overlay.
|
||||
if (rayPickResult.overlayID != _currentHoverOverOverlayID) {
|
||||
emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent);
|
||||
}
|
||||
|
||||
// Hover over current overlay.
|
||||
emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent);
|
||||
|
||||
_currentHoverOverOverlayID = rayPickResult.overlayID;
|
||||
}
|
||||
} else {
|
||||
// If previously hovering an overlay then leave hover.
|
||||
if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID) {
|
||||
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentHoverOverOverlayID));
|
||||
if (thisOverlay) {
|
||||
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
|
||||
emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent);
|
||||
}
|
||||
|
||||
_currentHoverOverOverlayID = UNKNOWN_OVERLAY_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,13 @@
|
|||
#ifndef hifi_Overlays_h
|
||||
#define hifi_Overlays_h
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QReadWriteLock>
|
||||
#include <QScriptValue>
|
||||
|
||||
#include "Overlay.h"
|
||||
#include <PointerEvent.h>
|
||||
|
||||
#include "Overlay.h"
|
||||
#include "OverlayPanel.h"
|
||||
#include "PanelAttachable.h"
|
||||
|
||||
|
@ -51,7 +53,7 @@ class RayToOverlayIntersectionResult {
|
|||
public:
|
||||
RayToOverlayIntersectionResult();
|
||||
bool intersects;
|
||||
int overlayID;
|
||||
unsigned int overlayID;
|
||||
float distance;
|
||||
BoxFace face;
|
||||
glm::vec3 surfaceNormal;
|
||||
|
@ -75,9 +77,13 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, R
|
|||
* @namespace Overlays
|
||||
*/
|
||||
|
||||
const unsigned int UNKNOWN_OVERLAY_ID = 0;
|
||||
|
||||
class Overlays : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(unsigned int keyboardFocusOverlay READ getKeyboardFocusOverlay WRITE setKeyboardFocusOverlay)
|
||||
|
||||
public:
|
||||
Overlays();
|
||||
|
||||
|
@ -94,6 +100,10 @@ public:
|
|||
unsigned int addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); }
|
||||
unsigned int addOverlay(Overlay::Pointer overlay);
|
||||
|
||||
void mousePressEvent(QMouseEvent* event);
|
||||
void mouseReleaseEvent(QMouseEvent* event);
|
||||
void mouseMoveEvent(QMouseEvent* event);
|
||||
|
||||
void cleanupAllOverlays();
|
||||
|
||||
public slots:
|
||||
|
@ -147,6 +157,15 @@ public slots:
|
|||
*/
|
||||
QString getOverlayType(unsigned int overlayId) const;
|
||||
|
||||
/**jsdoc
|
||||
* Get the overlay Script object.
|
||||
*
|
||||
* @function Overlays.getOverlayObject
|
||||
* @param {Overlays.OverlayID} overlayID The ID of the overlay to get the script object of.
|
||||
* @return {Object} The script object for the overlay if found.
|
||||
*/
|
||||
QObject* getOverlayObject(unsigned int id);
|
||||
|
||||
/**jsdoc
|
||||
* Get the ID of the overlay at a particular point on the HUD/screen.
|
||||
*
|
||||
|
@ -239,6 +258,17 @@ public slots:
|
|||
/// return true if there is a panel with that id else false
|
||||
bool isAddedPanel(unsigned int id) { return _panels.contains(id); }
|
||||
|
||||
void sendMousePressOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void sendMouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void sendMouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
|
||||
void sendHoverEnterOverlay(unsigned int id, PointerEvent event);
|
||||
void sendHoverOverOverlay(unsigned int id, PointerEvent event);
|
||||
void sendHoverLeaveOverlay(unsigned int id, PointerEvent event);
|
||||
|
||||
unsigned int getKeyboardFocusOverlay() const;
|
||||
void setKeyboardFocusOverlay(unsigned int id);
|
||||
|
||||
signals:
|
||||
/**jsdoc
|
||||
* Emitted when an overlay is deleted
|
||||
|
@ -249,6 +279,15 @@ signals:
|
|||
void overlayDeleted(unsigned int id);
|
||||
void panelDeleted(unsigned int id);
|
||||
|
||||
void mousePressOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void mouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void mouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void mousePressOffOverlay();
|
||||
|
||||
void hoverEnterOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void hoverOverOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
void hoverLeaveOverlay(unsigned int overlayID, const PointerEvent& event);
|
||||
|
||||
private:
|
||||
void cleanupOverlaysToDelete();
|
||||
|
||||
|
@ -262,8 +301,12 @@ private:
|
|||
QReadWriteLock _deleteLock;
|
||||
QScriptEngine* _scriptEngine;
|
||||
bool _enabled = true;
|
||||
|
||||
PointerEvent calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, RayToOverlayIntersectionResult rayPickResult,
|
||||
QMouseEvent* event, PointerEvent::EventType eventType);
|
||||
|
||||
unsigned int _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID };
|
||||
unsigned int _currentHoverOverOverlayID { UNKNOWN_OVERLAY_ID };
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif // hifi_Overlays_h
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//
|
||||
// Web3DOverlay.cpp
|
||||
//
|
||||
//
|
||||
// Created by Clement on 7/1/14.
|
||||
// Modified and renamed by Zander Otavka on 8/4/15
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
|
@ -12,8 +11,12 @@
|
|||
|
||||
#include "Web3DOverlay.h"
|
||||
|
||||
#include <Application.h>
|
||||
|
||||
#include <QQuickWindow>
|
||||
#include <QtGui/QOpenGLContext>
|
||||
#include <QtQuick/QQuickItem>
|
||||
#include <QtQml/QQmlContext>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <GeometryCache.h>
|
||||
|
@ -28,17 +31,24 @@
|
|||
|
||||
static const float DPI = 30.47f;
|
||||
static const float INCHES_TO_METERS = 1.0f / 39.3701f;
|
||||
static float OPAQUE_ALPHA_THRESHOLD = 0.99f;
|
||||
static const float METERS_TO_INCHES = 39.3701f;
|
||||
static const float OPAQUE_ALPHA_THRESHOLD = 0.99f;
|
||||
|
||||
QString const Web3DOverlay::TYPE = "web3d";
|
||||
|
||||
Web3DOverlay::Web3DOverlay() : _dpi(DPI) {
|
||||
_touchDevice.setCapabilities(QTouchDevice::Position);
|
||||
_touchDevice.setType(QTouchDevice::TouchScreen);
|
||||
_touchDevice.setName("RenderableWebEntityItemTouchDevice");
|
||||
_touchDevice.setMaximumTouchPoints(4);
|
||||
|
||||
_geometryId = DependencyManager::get<GeometryCache>()->allocateID();
|
||||
}
|
||||
|
||||
Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) :
|
||||
Billboard3DOverlay(Web3DOverlay),
|
||||
_url(Web3DOverlay->_url),
|
||||
_scriptURL(Web3DOverlay->_scriptURL),
|
||||
_dpi(Web3DOverlay->_dpi),
|
||||
_resolution(Web3DOverlay->_resolution)
|
||||
{
|
||||
|
@ -50,6 +60,19 @@ Web3DOverlay::~Web3DOverlay() {
|
|||
_webSurface->pause();
|
||||
_webSurface->disconnect(_connection);
|
||||
|
||||
QObject::disconnect(_mousePressConnection);
|
||||
_mousePressConnection = QMetaObject::Connection();
|
||||
QObject::disconnect(_mouseReleaseConnection);
|
||||
_mouseReleaseConnection = QMetaObject::Connection();
|
||||
QObject::disconnect(_mouseMoveConnection);
|
||||
_mouseMoveConnection = QMetaObject::Connection();
|
||||
QObject::disconnect(_hoverLeaveConnection);
|
||||
_hoverLeaveConnection = QMetaObject::Connection();
|
||||
|
||||
QObject::disconnect(_emitScriptEventConnection);
|
||||
_emitScriptEventConnection = QMetaObject::Connection();
|
||||
QObject::disconnect(_webEventReceivedConnection);
|
||||
_webEventReceivedConnection = QMetaObject::Connection();
|
||||
|
||||
// The lifetime of the QML surface MUST be managed by the main thread
|
||||
// Additionally, we MUST use local variables copied by value, rather than
|
||||
|
@ -67,11 +90,15 @@ Web3DOverlay::~Web3DOverlay() {
|
|||
}
|
||||
|
||||
void Web3DOverlay::update(float deltatime) {
|
||||
// FIXME: applyTransformTo causes tablet overlay to detach from tablet entity.
|
||||
// Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true?
|
||||
/*
|
||||
if (usecTimestampNow() > _transformExpiry) {
|
||||
Transform transform = getTransform();
|
||||
applyTransformTo(transform);
|
||||
setTransform(transform);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void Web3DOverlay::render(RenderArgs* args) {
|
||||
|
@ -92,21 +119,62 @@ void Web3DOverlay::render(RenderArgs* args) {
|
|||
// and the current rendering load)
|
||||
_webSurface->setMaxFps(10);
|
||||
_webSurface->create(currentContext);
|
||||
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/"));
|
||||
_webSurface->load("WebView.qml");
|
||||
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
|
||||
_webSurface->load("Web3DOverlay.qml");
|
||||
_webSurface->resume();
|
||||
_webSurface->getRootItem()->setProperty("url", _url);
|
||||
_webSurface->getRootItem()->setProperty("scriptURL", _scriptURL);
|
||||
_webSurface->getRootContext()->setContextProperty("ApplicationInterface", qApp);
|
||||
_webSurface->resize(QSize(_resolution.x, _resolution.y));
|
||||
currentContext->makeCurrent(currentSurface);
|
||||
|
||||
auto forwardPointerEvent = [=](unsigned int overlayID, const PointerEvent& event) {
|
||||
if (overlayID == getOverlayID()) {
|
||||
handlePointerEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
_mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, forwardPointerEvent);
|
||||
_mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, forwardPointerEvent);
|
||||
_mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, forwardPointerEvent);
|
||||
_hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay,
|
||||
[=](unsigned int overlayID, const PointerEvent& event) {
|
||||
if (this->_pressed && this->getOverlayID() == overlayID) {
|
||||
// If the user mouses off the overlay while the button is down, simulate a touch end.
|
||||
QTouchEvent::TouchPoint point;
|
||||
point.setId(event.getID());
|
||||
point.setState(Qt::TouchPointReleased);
|
||||
glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi);
|
||||
QPointF windowPoint(windowPos.x, windowPos.y);
|
||||
point.setScenePos(windowPoint);
|
||||
point.setPos(windowPoint);
|
||||
QList<QTouchEvent::TouchPoint> touchPoints;
|
||||
touchPoints.push_back(point);
|
||||
QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased,
|
||||
touchPoints);
|
||||
touchEvent->setWindow(_webSurface->getWindow());
|
||||
touchEvent->setDevice(&_touchDevice);
|
||||
touchEvent->setTarget(_webSurface->getRootItem());
|
||||
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
|
||||
}
|
||||
});
|
||||
|
||||
_emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent);
|
||||
_webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived);
|
||||
}
|
||||
|
||||
vec2 size = _resolution / _dpi * INCHES_TO_METERS;
|
||||
vec2 halfSize = size / 2.0f;
|
||||
vec2 halfSize = getSize() / 2.0f;
|
||||
vec4 color(toGlm(getColor()), getAlpha());
|
||||
|
||||
Transform transform = getTransform();
|
||||
|
||||
// FIXME: applyTransformTo causes tablet overlay to detach from tablet entity.
|
||||
// Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true?
|
||||
/*
|
||||
applyTransformTo(transform, true);
|
||||
setTransform(transform);
|
||||
*/
|
||||
|
||||
if (glm::length2(getDimensions()) != 1.0f) {
|
||||
transform.postScale(vec3(getDimensions(), 1.0f));
|
||||
}
|
||||
|
@ -144,6 +212,78 @@ const render::ShapeKey Web3DOverlay::getShapeKey() {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
QObject* Web3DOverlay::getEventHandler() {
|
||||
if (!_webSurface) {
|
||||
return nullptr;
|
||||
}
|
||||
return _webSurface->getEventHandler();
|
||||
}
|
||||
|
||||
void Web3DOverlay::setProxyWindow(QWindow* proxyWindow) {
|
||||
if (!_webSurface) {
|
||||
return;
|
||||
}
|
||||
|
||||
_webSurface->setProxyWindow(proxyWindow);
|
||||
}
|
||||
|
||||
void Web3DOverlay::handlePointerEvent(const PointerEvent& event) {
|
||||
if (!_webSurface) {
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi);
|
||||
QPointF windowPoint(windowPos.x, windowPos.y);
|
||||
|
||||
if (event.getType() == PointerEvent::Move) {
|
||||
// Forward a mouse move event to the Web surface.
|
||||
QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton,
|
||||
Qt::NoButton, Qt::NoModifier);
|
||||
QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent);
|
||||
}
|
||||
|
||||
if (event.getType() == PointerEvent::Press) {
|
||||
this->_pressed = true;
|
||||
} else if (event.getType() == PointerEvent::Release) {
|
||||
this->_pressed = false;
|
||||
}
|
||||
|
||||
QEvent::Type type;
|
||||
Qt::TouchPointState touchPointState;
|
||||
switch (event.getType()) {
|
||||
case PointerEvent::Press:
|
||||
type = QEvent::TouchBegin;
|
||||
touchPointState = Qt::TouchPointPressed;
|
||||
break;
|
||||
case PointerEvent::Release:
|
||||
type = QEvent::TouchEnd;
|
||||
touchPointState = Qt::TouchPointReleased;
|
||||
break;
|
||||
case PointerEvent::Move:
|
||||
default:
|
||||
type = QEvent::TouchUpdate;
|
||||
touchPointState = Qt::TouchPointMoved;
|
||||
break;
|
||||
}
|
||||
|
||||
QTouchEvent::TouchPoint point;
|
||||
point.setId(event.getID());
|
||||
point.setState(touchPointState);
|
||||
point.setPos(windowPoint);
|
||||
point.setScreenPos(windowPoint);
|
||||
QList<QTouchEvent::TouchPoint> touchPoints;
|
||||
touchPoints.push_back(point);
|
||||
|
||||
QTouchEvent* touchEvent = new QTouchEvent(type);
|
||||
touchEvent->setWindow(_webSurface->getWindow());
|
||||
touchEvent->setDevice(&_touchDevice);
|
||||
touchEvent->setTarget(_webSurface->getRootItem());
|
||||
touchEvent->setTouchPoints(touchPoints);
|
||||
touchEvent->setTouchPointStates(touchPointState);
|
||||
|
||||
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
|
||||
}
|
||||
|
||||
void Web3DOverlay::setProperties(const QVariantMap& properties) {
|
||||
Billboard3DOverlay::setProperties(properties);
|
||||
|
||||
|
@ -155,6 +295,14 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) {
|
|||
}
|
||||
}
|
||||
|
||||
auto scriptURLValue = properties["scriptURL"];
|
||||
if (scriptURLValue.isValid()) {
|
||||
QString newScriptURL = scriptURLValue.toString();
|
||||
if (newScriptURL != _scriptURL) {
|
||||
setScriptURL(newScriptURL);
|
||||
}
|
||||
}
|
||||
|
||||
auto resolution = properties["resolution"];
|
||||
if (resolution.isValid()) {
|
||||
bool valid;
|
||||
|
@ -164,7 +312,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
auto dpi = properties["dpi"];
|
||||
if (dpi.isValid()) {
|
||||
_dpi = dpi.toFloat();
|
||||
|
@ -175,6 +322,12 @@ QVariant Web3DOverlay::getProperty(const QString& property) {
|
|||
if (property == "url") {
|
||||
return _url;
|
||||
}
|
||||
if (property == "scriptURL") {
|
||||
return _scriptURL;
|
||||
}
|
||||
if (property == "resolution") {
|
||||
return vec2toVariant(_resolution);
|
||||
}
|
||||
if (property == "dpi") {
|
||||
return _dpi;
|
||||
}
|
||||
|
@ -188,22 +341,38 @@ void Web3DOverlay::setURL(const QString& url) {
|
|||
_webSurface->getRootItem()->setProperty("url", url);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Web3DOverlay::setScriptURL(const QString& scriptURL) {
|
||||
_scriptURL = scriptURL;
|
||||
if (_webSurface) {
|
||||
AbstractViewStateInterface::instance()->postLambdaEvent([this, scriptURL] {
|
||||
_webSurface->getRootItem()->setProperty("scriptURL", scriptURL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec2 Web3DOverlay::getSize() {
|
||||
return _resolution / _dpi * INCHES_TO_METERS * getDimensions();
|
||||
};
|
||||
|
||||
bool Web3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) {
|
||||
// FIXME - face and surfaceNormal not being returned
|
||||
|
||||
// Make sure position and rotation is updated.
|
||||
Transform transform;
|
||||
applyTransformTo(transform, true);
|
||||
setTransform(transform);
|
||||
// Don't call applyTransformTo() or setTransform() here because this code runs too frequently.
|
||||
|
||||
vec2 size = _resolution / _dpi * INCHES_TO_METERS * vec2(getDimensions());
|
||||
// Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale.
|
||||
return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), size, distance);
|
||||
return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), getSize(), distance);
|
||||
}
|
||||
|
||||
Web3DOverlay* Web3DOverlay::createClone() const {
|
||||
return new Web3DOverlay(this);
|
||||
}
|
||||
|
||||
void Web3DOverlay::emitScriptEvent(const QVariant& message) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
|
||||
} else {
|
||||
emit scriptEventReceived(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
|
||||
#include "Billboard3DOverlay.h"
|
||||
|
||||
#include <QTouchEvent>
|
||||
|
||||
#include <PointerEvent.h>
|
||||
|
||||
class OffscreenQmlSurface;
|
||||
|
||||
class Web3DOverlay : public Billboard3DOverlay {
|
||||
|
@ -29,25 +33,51 @@ public:
|
|||
|
||||
virtual void update(float deltatime) override;
|
||||
|
||||
QObject* getEventHandler();
|
||||
void setProxyWindow(QWindow* proxyWindow);
|
||||
void handlePointerEvent(const PointerEvent& event);
|
||||
|
||||
// setters
|
||||
void setURL(const QString& url);
|
||||
void setScriptURL(const QString& script);
|
||||
|
||||
void setProperties(const QVariantMap& properties) override;
|
||||
QVariant getProperty(const QString& property) override;
|
||||
|
||||
glm::vec2 getSize();
|
||||
|
||||
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance,
|
||||
BoxFace& face, glm::vec3& surfaceNormal) override;
|
||||
|
||||
virtual Web3DOverlay* createClone() const override;
|
||||
|
||||
public slots:
|
||||
void emitScriptEvent(const QVariant& scriptMessage);
|
||||
|
||||
signals:
|
||||
void scriptEventReceived(const QVariant& message);
|
||||
void webEventReceived(const QVariant& message);
|
||||
|
||||
private:
|
||||
QSharedPointer<OffscreenQmlSurface> _webSurface;
|
||||
QMetaObject::Connection _connection;
|
||||
gpu::TexturePointer _texture;
|
||||
QString _url;
|
||||
QString _scriptURL;
|
||||
float _dpi;
|
||||
vec2 _resolution{ 640, 480 };
|
||||
int _geometryId { 0 };
|
||||
|
||||
bool _pressed{ false };
|
||||
QTouchDevice _touchDevice;
|
||||
|
||||
QMetaObject::Connection _mousePressConnection;
|
||||
QMetaObject::Connection _mouseReleaseConnection;
|
||||
QMetaObject::Connection _mouseMoveConnection;
|
||||
QMetaObject::Connection _hoverLeaveConnection;
|
||||
|
||||
QMetaObject::Connection _emitScriptEventConnection;
|
||||
QMetaObject::Connection _webEventReceivedConnection;
|
||||
};
|
||||
|
||||
#endif // hifi_Web3DOverlay_h
|
||||
|
|
|
@ -836,6 +836,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
|
|||
// If the new hover entity does not match the previous hover entity then we are entering the new one
|
||||
// this is true if the _currentHoverOverEntityID is known or unknown
|
||||
if (rayPickResult.entityID != _currentHoverOverEntityID) {
|
||||
emit hoverEnterEntity(rayPickResult.entityID, pointerEvent);
|
||||
if (_entitiesScriptEngine) {
|
||||
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", pointerEvent);
|
||||
}
|
||||
|
|
|
@ -533,7 +533,8 @@ void RenderableModelEntityItem::update(const quint64& now) {
|
|||
properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it
|
||||
auto extents = _model->getMeshExtents();
|
||||
properties.setDimensions(extents.maximum - extents.minimum);
|
||||
qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL());
|
||||
qCDebug(entitiesrenderer) << "Autoresizing" << (!getName().isEmpty() ? getName() : getModelURL())
|
||||
<< "from mesh extents";
|
||||
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QUuid, getEntityItemID()),
|
||||
|
|
|
@ -326,8 +326,6 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) {
|
|||
touchEvent->setTouchPoints(touchPoints);
|
||||
touchEvent->setTouchPointStates(touchPointState);
|
||||
|
||||
_lastTouchEvent = *touchEvent;
|
||||
|
||||
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,9 +59,7 @@ private:
|
|||
QSharedPointer<OffscreenQmlSurface> _webSurface;
|
||||
QMetaObject::Connection _connection;
|
||||
gpu::TexturePointer _texture;
|
||||
ivec2 _lastPress { INT_MIN };
|
||||
bool _pressed{ false };
|
||||
QTouchEvent _lastTouchEvent { QEvent::TouchUpdate };
|
||||
uint64_t _lastRenderTime{ 0 };
|
||||
QTouchDevice _touchDevice;
|
||||
|
||||
|
|
|
@ -316,7 +316,7 @@ private:
|
|||
float _localRenderAlpha;
|
||||
bool _localRenderAlphaChanged;
|
||||
bool _defaultSettings;
|
||||
bool _dimensionsInitialized = true; // Only false if creating an entity localy with no dimensions properties
|
||||
bool _dimensionsInitialized = true; // Only false if creating an entity locally with no dimensions properties
|
||||
|
||||
// NOTE: The following are pseudo client only properties. They are only used in clients which can access
|
||||
// properties of model geometry. But these properties are not serialized like other properties.
|
||||
|
|
|
@ -35,6 +35,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
|
|||
connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
|
||||
connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged);
|
||||
connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged);
|
||||
connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged);
|
||||
}
|
||||
|
||||
void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
|
||||
|
@ -63,6 +64,11 @@ bool EntityScriptingInterface::canRezTmp() {
|
|||
return nodeList->getThisNodeCanRezTmp();
|
||||
}
|
||||
|
||||
bool EntityScriptingInterface::canWriteAssets() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
return nodeList->getThisNodeCanWriteAssets();
|
||||
}
|
||||
|
||||
void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
|
||||
if (_entityTree) {
|
||||
disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity);
|
||||
|
|
|
@ -110,6 +110,12 @@ public slots:
|
|||
*/
|
||||
Q_INVOKABLE bool canRezTmp();
|
||||
|
||||
/**jsdoc
|
||||
* @function Entities.canWriteAsseets
|
||||
* @return {bool} `true` if the DomainServer will allow this Node/Avatar to write to the asset server
|
||||
*/
|
||||
Q_INVOKABLE bool canWriteAssets();
|
||||
|
||||
/**jsdoc
|
||||
* Add a new entity with the specified properties. If `clientOnly` is true, the entity will
|
||||
* not be sent to the server and will only be visible/accessible on the local client.
|
||||
|
@ -282,6 +288,7 @@ signals:
|
|||
void canAdjustLocksChanged(bool canAdjustLocks);
|
||||
void canRezChanged(bool canRez);
|
||||
void canRezTmpChanged(bool canRez);
|
||||
void canWriteAssetsChanged(bool canWriteAssets);
|
||||
|
||||
void mousePressOnEntity(const EntityItemID& entityItemID, const PointerEvent& event);
|
||||
void mouseMoveOnEntity(const EntityItemID& entityItemID, const PointerEvent& event);
|
||||
|
|
|
@ -26,3 +26,16 @@ void ResourceRequest::send() {
|
|||
_state = InProgress;
|
||||
doSend();
|
||||
}
|
||||
|
||||
QString ResourceRequest::getResultString() const {
|
||||
switch (_result) {
|
||||
case Success: return "Success";
|
||||
case Error: return "Error";
|
||||
case Timeout: return "Timeout";
|
||||
case ServerUnavailable: return "Server Unavailable";
|
||||
case AccessDenied: return "Access Denied";
|
||||
case InvalidURL: return "Invalid URL";
|
||||
case NotFound: return "Not Found";
|
||||
default: return "Unspecified Error";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ public:
|
|||
QByteArray getData() { return _data; }
|
||||
State getState() const { return _state; }
|
||||
Result getResult() const { return _result; }
|
||||
QString getResultString() const;
|
||||
QUrl getUrl() const { return _url; }
|
||||
bool loadedFromCache() const { return _loadedFromCache; }
|
||||
|
||||
|
|
|
@ -304,6 +304,12 @@ template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointe
|
|||
}
|
||||
return Item::Bound();
|
||||
}
|
||||
template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload) {
|
||||
if (payload) {
|
||||
return payload->getLayer();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload) {
|
||||
if (payload) {
|
||||
|
@ -378,6 +384,10 @@ ItemKey ModelMeshPartPayload::getKey() const {
|
|||
builder.withInvisible();
|
||||
}
|
||||
|
||||
if (_model->isLayeredInFront()) {
|
||||
builder.withLayered();
|
||||
}
|
||||
|
||||
if (_isBlendShaped || _isSkinned) {
|
||||
builder.withDeformed();
|
||||
}
|
||||
|
@ -396,6 +406,17 @@ ItemKey ModelMeshPartPayload::getKey() const {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
int ModelMeshPartPayload::getLayer() const {
|
||||
// MAgic number while we are defining the layering mechanism:
|
||||
const int LAYER_3D_FRONT = 1;
|
||||
const int LAYER_3D = 0;
|
||||
if (_model->isLayeredInFront()) {
|
||||
return LAYER_3D_FRONT;
|
||||
} else {
|
||||
return LAYER_3D;
|
||||
}
|
||||
}
|
||||
|
||||
ShapeKey ModelMeshPartPayload::getShapeKey() const {
|
||||
|
||||
// guard against partially loaded meshes
|
||||
|
|
|
@ -95,6 +95,7 @@ public:
|
|||
|
||||
// Render Item interface
|
||||
render::ItemKey getKey() const override;
|
||||
int getLayer() const;
|
||||
render::ShapeKey getShapeKey() const override; // shape interface
|
||||
void render(RenderArgs* args) const override;
|
||||
|
||||
|
@ -122,6 +123,7 @@ private:
|
|||
namespace render {
|
||||
template <> const ItemKey payloadGetKey(const ModelMeshPartPayload::Pointer& payload);
|
||||
template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointer& payload);
|
||||
template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload);
|
||||
template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload);
|
||||
template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, RenderArgs* args);
|
||||
}
|
||||
|
|
|
@ -620,6 +620,22 @@ void Model::setVisibleInScene(bool newValue, std::shared_ptr<render::Scene> scen
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void Model::setLayeredInFront(bool layered, std::shared_ptr<render::Scene> scene) {
|
||||
if (_isLayeredInFront != layered) {
|
||||
_isLayeredInFront = layered;
|
||||
|
||||
render::PendingChanges pendingChanges;
|
||||
foreach(auto item, _modelMeshRenderItems.keys()) {
|
||||
pendingChanges.resetItem(item, _modelMeshRenderItems[item]);
|
||||
}
|
||||
foreach(auto item, _collisionRenderItems.keys()) {
|
||||
pendingChanges.resetItem(item, _collisionRenderItems[item]);
|
||||
}
|
||||
scene->enqueuePendingChanges(pendingChanges);
|
||||
}
|
||||
}
|
||||
|
||||
bool Model::addToScene(std::shared_ptr<render::Scene> scene,
|
||||
render::PendingChanges& pendingChanges,
|
||||
render::Item::Status::Getters& statusGetters) {
|
||||
|
|
|
@ -80,6 +80,7 @@ public:
|
|||
|
||||
// new Scene/Engine rendering support
|
||||
void setVisibleInScene(bool newValue, std::shared_ptr<render::Scene> scene);
|
||||
void setLayeredInFront(bool layered, std::shared_ptr<render::Scene> scene);
|
||||
bool needsFixupInScene() const;
|
||||
|
||||
bool needsReload() const { return _needsReload; }
|
||||
|
@ -98,6 +99,8 @@ public:
|
|||
|
||||
bool isVisible() const { return _isVisible; }
|
||||
|
||||
bool isLayeredInFront() const { return _isLayeredInFront; }
|
||||
|
||||
void updateRenderItems();
|
||||
void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; }
|
||||
bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; }
|
||||
|
@ -413,6 +416,8 @@ protected:
|
|||
int _renderInfoDrawCalls { 0 };
|
||||
int _renderInfoHasTransparent { false };
|
||||
|
||||
bool _isLayeredInFront { false };
|
||||
|
||||
private:
|
||||
float _loadingPriority { 0.0f };
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ FileScriptingInterface::FileScriptingInterface(QObject* parent) : QObject(parent
|
|||
// nothing for now
|
||||
}
|
||||
|
||||
void FileScriptingInterface::runUnzip(QString path, QUrl url) {
|
||||
void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd) {
|
||||
qCDebug(scriptengine) << "Url that was downloaded: " + url.toString();
|
||||
qCDebug(scriptengine) << "Path where download is saved: " + path;
|
||||
QString fileName = "/" + path.section("/", -1);
|
||||
|
@ -45,15 +45,13 @@ void FileScriptingInterface::runUnzip(QString path, QUrl url) {
|
|||
}
|
||||
|
||||
QString file = unzipFile(path, tempDir);
|
||||
QString filename = QUrl::fromLocalFile(file).toString();
|
||||
if (file != "") {
|
||||
qCDebug(scriptengine) << "Object file to upload: " + file;
|
||||
QUrl url = QUrl::fromLocalFile(file);
|
||||
emit unzipSuccess(url.toString());
|
||||
qCDebug(scriptengine) << "File to upload: " + filename;
|
||||
} else {
|
||||
qCDebug(scriptengine) << "unzip failed";
|
||||
qCDebug(scriptengine) << "Unzip failed";
|
||||
}
|
||||
qCDebug(scriptengine) << "Removing temporary directory at: " + tempDir;
|
||||
QDir(tempDir).removeRecursively();
|
||||
emit unzipResult(path, filename, autoAdd);
|
||||
}
|
||||
|
||||
// fix to check that we are only referring to a temporary directory
|
||||
|
@ -69,21 +67,6 @@ bool FileScriptingInterface::isTempDir(QString tempDir) {
|
|||
return (testContainer == tempContainer);
|
||||
}
|
||||
|
||||
// checks whether the webview is displaying a Clara.io page for Marketplaces.qml
|
||||
bool FileScriptingInterface::isClaraLink(QUrl url) {
|
||||
return (url.toString().contains("clara.io") && !url.toString().contains("clara.io/signup"));
|
||||
}
|
||||
|
||||
bool FileScriptingInterface::isZippedFbx(QUrl url) {
|
||||
return (url.toString().endsWith("fbx.zip"));
|
||||
}
|
||||
|
||||
// checks whether a user tries to download a file that is not in .fbx format
|
||||
bool FileScriptingInterface::isZipped(QUrl url) {
|
||||
return (url.toString().endsWith(".zip"));
|
||||
}
|
||||
|
||||
// this function is not in use
|
||||
QString FileScriptingInterface::getTempDir() {
|
||||
QTemporaryDir dir;
|
||||
dir.setAutoRemove(false);
|
||||
|
@ -109,7 +92,6 @@ void FileScriptingInterface::downloadZip(QString path, const QString link) {
|
|||
request->send();
|
||||
}
|
||||
|
||||
|
||||
QString FileScriptingInterface::unzipFile(QString path, QString tempDir) {
|
||||
|
||||
QDir dir(path);
|
||||
|
|
|
@ -22,17 +22,13 @@ class FileScriptingInterface : public QObject {
|
|||
public:
|
||||
FileScriptingInterface(QObject* parent);
|
||||
|
||||
|
||||
public slots:
|
||||
bool isZippedFbx(QUrl url);
|
||||
bool isZipped(QUrl url);
|
||||
bool isClaraLink(QUrl url);
|
||||
QString convertUrlToPath(QUrl url);
|
||||
void runUnzip(QString path, QUrl url);
|
||||
void runUnzip(QString path, QUrl url, bool autoAdd);
|
||||
QString getTempDir();
|
||||
|
||||
signals:
|
||||
void unzipSuccess(QString url);
|
||||
void unzipResult(QString zipFile, QString unzipFile, bool autoAdd);
|
||||
|
||||
private:
|
||||
bool isTempDir(QString tempDir);
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include "OffscreenUi.h"
|
||||
|
||||
static const char* const URL_PROPERTY = "source";
|
||||
static const char* const SCRIPT_PROPERTY = "scriptUrl";
|
||||
|
||||
// Method called by Qt scripts to create a new web window in the overlay
|
||||
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
|
||||
|
@ -31,51 +32,6 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi
|
|||
return engine->newQObject(retVal);
|
||||
}
|
||||
|
||||
void QmlWebWindowClass::emitScriptEvent(const QVariant& scriptMessage) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage));
|
||||
} else {
|
||||
emit scriptEventReceived(scriptMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void QmlWebWindowClass::emitWebEvent(const QVariant& webMessage) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage));
|
||||
} else {
|
||||
// Special case to handle raising and lowering the virtual keyboard.
|
||||
const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD";
|
||||
const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC";
|
||||
const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD";
|
||||
QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : "";
|
||||
if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) {
|
||||
setKeyboardRaised(asQuickItem(), true, messageString == RAISE_KEYBOARD_NUMERIC);
|
||||
} else if (messageString == LOWER_KEYBOARD) {
|
||||
setKeyboardRaised(asQuickItem(), false);
|
||||
} else {
|
||||
emit webEventReceived(webMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QmlWebWindowClass::setKeyboardRaised(QObject* object, bool raised, bool numeric) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
|
||||
while (item) {
|
||||
if (item->property("keyboardRaised").isValid()) {
|
||||
if (item->property("punctuationMode").isValid()) {
|
||||
item->setProperty("punctuationMode", QVariant(numeric));
|
||||
}
|
||||
item->setProperty("keyboardRaised", QVariant(raised));
|
||||
return;
|
||||
}
|
||||
item = dynamic_cast<QQuickItem*>(item->parentItem());
|
||||
}
|
||||
}
|
||||
|
||||
QString QmlWebWindowClass::getURL() const {
|
||||
QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant {
|
||||
if (_qmlWindow.isNull()) {
|
||||
|
@ -93,3 +49,11 @@ void QmlWebWindowClass::setURL(const QString& urlString) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
void QmlWebWindowClass::setScriptURL(const QString& script) {
|
||||
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
|
||||
if (!_qmlWindow.isNull()) {
|
||||
_qmlWindow->setProperty(SCRIPT_PROPERTY, script);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,20 +22,13 @@ public:
|
|||
public slots:
|
||||
QString getURL() const;
|
||||
void setURL(const QString& url);
|
||||
|
||||
void emitScriptEvent(const QVariant& scriptMessage);
|
||||
void emitWebEvent(const QVariant& webMessage);
|
||||
void setScriptURL(const QString& script);
|
||||
|
||||
signals:
|
||||
void urlChanged();
|
||||
void scriptEventReceived(const QVariant& message);
|
||||
void webEventReceived(const QVariant& message);
|
||||
|
||||
protected:
|
||||
QString qmlSource() const override { return "QmlWebWindow.qml"; }
|
||||
|
||||
private:
|
||||
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -150,6 +150,52 @@ void QmlWindowClass::sendToQml(const QVariant& message) {
|
|||
QMetaObject::invokeMethod(asQuickItem(), "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message));
|
||||
}
|
||||
|
||||
|
||||
void QmlWindowClass::emitScriptEvent(const QVariant& scriptMessage) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage));
|
||||
} else {
|
||||
emit scriptEventReceived(scriptMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void QmlWindowClass::emitWebEvent(const QVariant& webMessage) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage));
|
||||
} else {
|
||||
// Special case to handle raising and lowering the virtual keyboard.
|
||||
const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD";
|
||||
const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC";
|
||||
const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD";
|
||||
QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : "";
|
||||
if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) {
|
||||
setKeyboardRaised(asQuickItem(), true, messageString == RAISE_KEYBOARD_NUMERIC);
|
||||
} else if (messageString == LOWER_KEYBOARD) {
|
||||
setKeyboardRaised(asQuickItem(), false);
|
||||
} else {
|
||||
emit webEventReceived(webMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QmlWindowClass::setKeyboardRaised(QObject* object, bool raised, bool numeric) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
|
||||
while (item) {
|
||||
if (item->property("keyboardRaised").isValid()) {
|
||||
if (item->property("punctuationMode").isValid()) {
|
||||
item->setProperty("punctuationMode", QVariant(numeric));
|
||||
}
|
||||
item->setProperty("keyboardRaised", QVariant(raised));
|
||||
return;
|
||||
}
|
||||
item = dynamic_cast<QQuickItem*>(item->parentItem());
|
||||
}
|
||||
}
|
||||
|
||||
QmlWindowClass::~QmlWindowClass() {
|
||||
close();
|
||||
}
|
||||
|
|
|
@ -51,6 +51,10 @@ public slots:
|
|||
// Scripts can use this to send a message to the QML object
|
||||
void sendToQml(const QVariant& message);
|
||||
|
||||
// QmlWindow content may include WebView requiring EventBridge.
|
||||
void emitScriptEvent(const QVariant& scriptMessage);
|
||||
void emitWebEvent(const QVariant& webMessage);
|
||||
|
||||
signals:
|
||||
void visibleChanged();
|
||||
void positionChanged();
|
||||
|
@ -61,6 +65,10 @@ signals:
|
|||
// Scripts can connect to this signal to receive messages from the QML object
|
||||
void fromQml(const QVariant& message);
|
||||
|
||||
// QmlWindow content may include WebView requiring EventBridge.
|
||||
void scriptEventReceived(const QVariant& message);
|
||||
void webEventReceived(const QVariant& message);
|
||||
|
||||
protected slots:
|
||||
void hasMoved(QVector2D);
|
||||
void hasClosed();
|
||||
|
@ -81,6 +89,10 @@ protected:
|
|||
bool _toolWindow { false };
|
||||
QPointer<QObject> _qmlWindow;
|
||||
QString _source;
|
||||
|
||||
private:
|
||||
// QmlWindow content may include WebView requiring EventBridge.
|
||||
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -140,6 +140,10 @@ var ONE_VEC = {
|
|||
|
||||
var NULL_UUID = "{00000000-0000-0000-0000-000000000000}";
|
||||
|
||||
var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
|
||||
var INCHES_TO_METERS = 1.0 / 39.3701;
|
||||
|
||||
|
||||
// these control how long an abandoned pointer line or action will hang around
|
||||
var ACTION_TTL = 15; // seconds
|
||||
var ACTION_TTL_REFRESH = 5;
|
||||
|
@ -186,6 +190,7 @@ var STATE_NEAR_TRIGGER = 4;
|
|||
var STATE_FAR_TRIGGER = 5;
|
||||
var STATE_HOLD = 6;
|
||||
var STATE_ENTITY_TOUCHING = 7;
|
||||
var STATE_OVERLAY_TOUCHING = 8;
|
||||
|
||||
var holdEnabled = true;
|
||||
var nearGrabEnabled = true;
|
||||
|
@ -208,6 +213,8 @@ var mostRecentSearchingHand = RIGHT_HAND;
|
|||
|
||||
var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx";
|
||||
|
||||
var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse.
|
||||
|
||||
CONTROLLER_STATE_MACHINE[STATE_OFF] = {
|
||||
name: "off",
|
||||
enterMethod: "offEnter",
|
||||
|
@ -249,6 +256,12 @@ CONTROLLER_STATE_MACHINE[STATE_ENTITY_TOUCHING] = {
|
|||
exitMethod: "entityTouchingExit",
|
||||
updateMethod: "entityTouching"
|
||||
};
|
||||
CONTROLLER_STATE_MACHINE[STATE_OVERLAY_TOUCHING] = {
|
||||
name: "overlayTouching",
|
||||
enterMethod: "overlayTouchingEnter",
|
||||
exitMethod: "overlayTouchingExit",
|
||||
updateMethod: "overlayTouching"
|
||||
};
|
||||
|
||||
function distanceBetweenPointAndEntityBoundingBox(point, entityProps) {
|
||||
var entityXform = new Xform(entityProps.rotation, entityProps.position);
|
||||
|
@ -273,27 +286,48 @@ function angleBetween(a, b) {
|
|||
return Math.acos(Vec3.dot(Vec3.normalize(a), Vec3.normalize(b)));
|
||||
}
|
||||
|
||||
function projectOntoEntityXYPlane(entityID, worldPos) {
|
||||
var props = entityPropertiesCache.getProps(entityID);
|
||||
var invRot = Quat.inverse(props.rotation);
|
||||
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, props.position));
|
||||
var invDimensions = { x: 1 / props.dimensions.x,
|
||||
y: 1 / props.dimensions.y,
|
||||
z: 1 / props.dimensions.z };
|
||||
var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint);
|
||||
return { x: normalizedPos.x * props.dimensions.x,
|
||||
y: (1 - normalizedPos.y) * props.dimensions.y }; // flip y-axis
|
||||
function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrationPoint) {
|
||||
var invRot = Quat.inverse(rotation);
|
||||
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position));
|
||||
var invDimensions = { x: 1 / dimensions.x,
|
||||
y: 1 / dimensions.y,
|
||||
z: 1 / dimensions.z };
|
||||
var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint);
|
||||
return { x: normalizedPos.x * dimensions.x,
|
||||
y: (1 - normalizedPos.y) * dimensions.y }; // flip y-axis
|
||||
}
|
||||
|
||||
function handLaserIntersectEntity(entityID, start) {
|
||||
function projectOntoEntityXYPlane(entityID, worldPos) {
|
||||
var props = entityPropertiesCache.getProps(entityID);
|
||||
return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint);
|
||||
}
|
||||
|
||||
function projectOntoOverlayXYPlane(overlayID, worldPos) {
|
||||
var position = Overlays.getProperty(overlayID, "position");
|
||||
var rotation = Overlays.getProperty(overlayID, "rotation");
|
||||
var dimensions;
|
||||
|
||||
var dpi = Overlays.getProperty(overlayID, "dpi");
|
||||
if (dpi) {
|
||||
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale.
|
||||
var resolution = Overlays.getProperty(overlayID, "resolution");
|
||||
resolution.z = 1; // Circumvent divide-by-zero.
|
||||
var scale = Overlays.getProperty(overlayID, "dimensions");
|
||||
dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale);
|
||||
} else {
|
||||
dimensions = Overlays.getProperty(overlayID, "dimensions");
|
||||
}
|
||||
|
||||
return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT);
|
||||
}
|
||||
|
||||
function handLaserIntersectItem(position, rotation, start) {
|
||||
var worldHandPosition = start.position;
|
||||
var worldHandRotation = start.orientation;
|
||||
|
||||
var props = entityPropertiesCache.getProps(entityID);
|
||||
|
||||
if (props.position) {
|
||||
var planePosition = props.position;
|
||||
var planeNormal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1.0});
|
||||
if (position) {
|
||||
var planePosition = position;
|
||||
var planeNormal = Vec3.multiplyQbyV(rotation, {x: 0, y: 0, z: 1.0});
|
||||
var rayStart = worldHandPosition;
|
||||
var rayDirection = Quat.getUp(worldHandRotation);
|
||||
var intersectionInfo = rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection);
|
||||
|
@ -318,6 +352,17 @@ function handLaserIntersectEntity(entityID, start) {
|
|||
}
|
||||
}
|
||||
|
||||
function handLaserIntersectEntity(entityID, start) {
|
||||
var props = entityPropertiesCache.getProps(entityID);
|
||||
return handLaserIntersectItem(props.position, props.rotation, start);
|
||||
}
|
||||
|
||||
function handLaserIntersectOverlay(overlayID, start) {
|
||||
var position = Overlays.getProperty(overlayID, "position");
|
||||
var rotation = Overlays.getProperty(overlayID, "rotation");
|
||||
return handLaserIntersectItem(position, rotation, start);
|
||||
}
|
||||
|
||||
function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
|
||||
var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
|
||||
if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {
|
||||
|
@ -727,6 +772,7 @@ function MyController(hand) {
|
|||
|
||||
this.actionID = null; // action this script created...
|
||||
this.grabbedEntity = null; // on this entity.
|
||||
this.grabbedOverlay = null;
|
||||
this.state = STATE_OFF;
|
||||
this.pointer = null; // entity-id of line object
|
||||
this.entityActivated = false;
|
||||
|
@ -1159,6 +1205,7 @@ function MyController(hand) {
|
|||
|
||||
var result = {
|
||||
entityID: null,
|
||||
overlayID: null,
|
||||
searchRay: pickRay,
|
||||
distance: PICK_MAX_DISTANCE
|
||||
};
|
||||
|
@ -1424,6 +1471,7 @@ function MyController(hand) {
|
|||
var name;
|
||||
|
||||
this.grabbedEntity = null;
|
||||
this.grabbedOverlay = null;
|
||||
this.isInitialGrab = false;
|
||||
this.shouldResetParentOnRelease = false;
|
||||
|
||||
|
@ -1517,6 +1565,7 @@ function MyController(hand) {
|
|||
name = entityPropertiesCache.getProps(entity).name;
|
||||
|
||||
if (Entities.keyboardFocusEntity != entity) {
|
||||
Overlays.keyboardFocusOverlay = 0;
|
||||
Entities.keyboardFocusEntity = entity;
|
||||
|
||||
pointerEvent = {
|
||||
|
@ -1536,7 +1585,8 @@ function MyController(hand) {
|
|||
// send mouse events for button highlights and tooltips.
|
||||
if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand &&
|
||||
this.getOtherHandController().state !== STATE_SEARCHING &&
|
||||
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING)) {
|
||||
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING &&
|
||||
this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) {
|
||||
|
||||
// most recently searching hand has priority over other hand, for the purposes of button highlighting.
|
||||
pointerEvent = {
|
||||
|
@ -1589,6 +1639,65 @@ function MyController(hand) {
|
|||
}
|
||||
}
|
||||
|
||||
var overlay;
|
||||
|
||||
if (rayPickInfo.overlayID) {
|
||||
overlay = rayPickInfo.overlayID;
|
||||
|
||||
if (Overlays.keyboardFocusOverlay != overlay) {
|
||||
Entities.keyboardFocusEntity = null;
|
||||
Overlays.keyboardFocusOverlay = overlay;
|
||||
|
||||
pointerEvent = {
|
||||
type: "Move",
|
||||
id: HARDWARE_MOUSE_ID,
|
||||
pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection),
|
||||
pos3D: rayPickInfo.intersection,
|
||||
normal: rayPickInfo.normal,
|
||||
direction: rayPickInfo.searchRay.direction,
|
||||
button: "None"
|
||||
};
|
||||
|
||||
this.hoverOverlay = overlay;
|
||||
Overlays.sendHoverEnterOverlay(overlay, pointerEvent);
|
||||
}
|
||||
|
||||
// Send mouse events for button highlights and tooltips.
|
||||
if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand &&
|
||||
this.getOtherHandController().state !== STATE_SEARCHING &&
|
||||
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING &&
|
||||
this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) {
|
||||
|
||||
// most recently searching hand has priority over other hand, for the purposes of button highlighting.
|
||||
pointerEvent = {
|
||||
type: "Move",
|
||||
id: HARDWARE_MOUSE_ID,
|
||||
pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection),
|
||||
pos3D: rayPickInfo.intersection,
|
||||
normal: rayPickInfo.normal,
|
||||
direction: rayPickInfo.searchRay.direction,
|
||||
button: "None"
|
||||
};
|
||||
|
||||
Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent);
|
||||
Overlays.sendHoverOverOverlay(overlay, pointerEvent);
|
||||
}
|
||||
|
||||
if (this.triggerSmoothedGrab() && !isEditing()) {
|
||||
this.grabbedOverlay = overlay;
|
||||
this.setState(STATE_OVERLAY_TOUCHING, "begin touching overlay '" + overlay + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (this.hoverOverlay) {
|
||||
pointerEvent = {
|
||||
type: "Move",
|
||||
id: HARDWARE_MOUSE_ID
|
||||
};
|
||||
Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent);
|
||||
this.hoverOverlay = null;
|
||||
}
|
||||
|
||||
this.updateEquipHaptics(potentialEquipHotspot, handPosition);
|
||||
|
||||
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS);
|
||||
|
@ -2339,7 +2448,6 @@ function MyController(hand) {
|
|||
Entities.sendClickReleaseOnEntity(this.grabbedEntity, pointerEvent);
|
||||
Entities.sendHoverLeaveEntity(this.grabbedEntity, pointerEvent);
|
||||
}
|
||||
this.focusedEntity = null;
|
||||
};
|
||||
|
||||
this.entityTouching = function(dt) {
|
||||
|
@ -2359,6 +2467,7 @@ function MyController(hand) {
|
|||
if (intersectInfo) {
|
||||
|
||||
if (Entities.keyboardFocusEntity != this.grabbedEntity) {
|
||||
Overlays.keyboardFocusOverlay = 0;
|
||||
Entities.keyboardFocusEntity = this.grabbedEntity;
|
||||
}
|
||||
|
||||
|
@ -2393,6 +2502,108 @@ function MyController(hand) {
|
|||
}
|
||||
};
|
||||
|
||||
this.overlayTouchingEnter = function () {
|
||||
// Test for intersection between controller laser and Web overlay plane.
|
||||
var intersectInfo =
|
||||
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
|
||||
if (intersectInfo) {
|
||||
var pointerEvent = {
|
||||
type: "Press",
|
||||
id: HARDWARE_MOUSE_ID,
|
||||
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
|
||||
pos3D: intersectInfo.point,
|
||||
normal: intersectInfo.normal,
|
||||
direction: intersectInfo.searchRay.direction,
|
||||
button: "Primary",
|
||||
isPrimaryHeld: true
|
||||
};
|
||||
|
||||
|
||||
Overlays.sendMousePressOnOverlay(this.grabbedOverlay, pointerEvent);
|
||||
|
||||
this.touchingEnterTimer = 0;
|
||||
this.touchingEnterPointerEvent = pointerEvent;
|
||||
this.touchingEnterPointerEvent.button = "None";
|
||||
this.deadspotExpired = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.overlayTouchingExit = function () {
|
||||
// Test for intersection between controller laser and Web overlay plane.
|
||||
var intersectInfo =
|
||||
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
|
||||
if (intersectInfo) {
|
||||
var pointerEvent;
|
||||
if (this.deadspotExpired) {
|
||||
pointerEvent = {
|
||||
type: "Release",
|
||||
id: HARDWARE_MOUSE_ID,
|
||||
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
|
||||
pos3D: intersectInfo.point,
|
||||
normal: intersectInfo.normal,
|
||||
direction: intersectInfo.searchRay.direction,
|
||||
button: "Primary"
|
||||
};
|
||||
} else {
|
||||
pointerEvent = this.touchingEnterPointerEvent;
|
||||
pointerEvent.type = "Release";
|
||||
pointerEvent.button = "Primary";
|
||||
pointerEvent.isPrimaryHeld = false;
|
||||
}
|
||||
|
||||
Overlays.sendMouseReleaseOnOverlay(this.grabbedOverlay, pointerEvent);
|
||||
Overlays.sendHoverLeaveOverlay(this.grabbedOverlay, pointerEvent);
|
||||
}
|
||||
};
|
||||
|
||||
this.overlayTouching = function (dt) {
|
||||
this.touchingEnterTimer += dt;
|
||||
|
||||
if (!this.triggerSmoothedGrab()) {
|
||||
this.setState(STATE_OFF, "released trigger");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test for intersection between controller laser and Web overlay plane.
|
||||
var intersectInfo =
|
||||
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
|
||||
if (intersectInfo) {
|
||||
|
||||
if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) {
|
||||
Entities.keyboardFocusEntity = null;
|
||||
Overlays.keyboardFocusOverlay = this.grabbedOverlay;
|
||||
}
|
||||
|
||||
var pointerEvent = {
|
||||
type: "Move",
|
||||
id: HARDWARE_MOUSE_ID,
|
||||
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
|
||||
pos3D: intersectInfo.point,
|
||||
normal: intersectInfo.normal,
|
||||
direction: intersectInfo.searchRay.direction,
|
||||
button: "NoButtons",
|
||||
isPrimaryHeld: true
|
||||
};
|
||||
|
||||
var POINTER_PRESS_TO_MOVE_DELAY = 0.15; // seconds
|
||||
var POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.05; // radians ~ 3 degrees
|
||||
if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY ||
|
||||
angleBetween(pointerEvent.direction, this.touchingEnterPointerEvent.direction) > POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE) {
|
||||
Overlays.sendMouseMoveOnOverlay(this.grabbedOverlay, pointerEvent);
|
||||
this.deadspotExpired = true;
|
||||
}
|
||||
|
||||
this.intersectionDistance = intersectInfo.distance;
|
||||
if (farGrabEnabled) {
|
||||
this.searchIndicatorOn(intersectInfo.searchRay);
|
||||
}
|
||||
Reticle.setVisible(false);
|
||||
} else {
|
||||
this.setState(STATE_OFF, "grabbed overlay was destroyed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
this.release = function() {
|
||||
this.turnOffVisualizations();
|
||||
|
||||
|
@ -2434,6 +2645,7 @@ function MyController(hand) {
|
|||
|
||||
this.actionID = null;
|
||||
this.grabbedEntity = null;
|
||||
this.grabbedOverlay = null;
|
||||
this.grabbedHotspot = null;
|
||||
|
||||
if (this.triggerSmoothedGrab()) {
|
||||
|
|
|
@ -334,9 +334,14 @@ input[type=button].red {
|
|||
}
|
||||
input[type=button].blue {
|
||||
color: #fff;
|
||||
background-color: #94132e;
|
||||
background-color: #1080b8;
|
||||
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
|
||||
}
|
||||
input[type=button].white {
|
||||
color: #121212;
|
||||
background-color: #afafaf;
|
||||
background: linear-gradient(#fff 20%, #afafaf 100%);
|
||||
}
|
||||
|
||||
input[type=button]:enabled:hover {
|
||||
background: linear-gradient(#000, #000);
|
||||
|
@ -350,6 +355,10 @@ input[type=button].blue:enabled:hover {
|
|||
background: linear-gradient(#00b4ef, #00b4ef);
|
||||
border: none;
|
||||
}
|
||||
input[type=button].white:enabled:hover {
|
||||
background: linear-gradient(#fff, #fff);
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type=button]:active {
|
||||
background: linear-gradient(#343434, #343434);
|
||||
|
@ -360,6 +369,9 @@ input[type=button].red:active {
|
|||
input[type=button].blue:active {
|
||||
background: linear-gradient(#1080b8, #1080b8);
|
||||
}
|
||||
input[type=button].white:active {
|
||||
background: linear-gradient(#afafaf, #afafaf);
|
||||
}
|
||||
|
||||
input[type=button]:disabled {
|
||||
color: #252525;
|
||||
|
|
|
@ -22,11 +22,12 @@ body {
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
.marketplaces-intro-text {
|
||||
margin-bottom: 60px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.marketplace-tile {
|
||||
float:left;
|
||||
width: 100%;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.marketplace-tile-first-column {
|
||||
text-align: center;
|
||||
|
@ -75,6 +76,44 @@ body {
|
|||
.marketplace-clara-steps > li {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#marketplace-navigation {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #00b4ef;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
#marketplace-navigation .glyph {
|
||||
/*
|
||||
// Target look but can't use font in injected script.
|
||||
font-family: HiFi-Glyphs;
|
||||
font-size: 40px;
|
||||
margin-left: 20px;
|
||||
*/
|
||||
font-family: sans-serif;
|
||||
font-size: 24px;
|
||||
margin-left: 20px;
|
||||
margin-right: 3px;
|
||||
color: #fff;
|
||||
line-height: 50px;
|
||||
}
|
||||
#marketplace-navigation .text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
line-height: 50px;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
#marketplace-navigation input {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
margin-top: 12px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
.marketplace-tile-first-column {
|
||||
float: left;
|
||||
|
@ -89,10 +128,10 @@ body {
|
|||
text-align:center;
|
||||
}
|
||||
.tile-divider {
|
||||
width: 100%;
|
||||
margin-left: 0%;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
.marketplace-tile-image {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
.marketplace-tile-image{
|
||||
margin-bottom:15px;
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ function setUpKeyboardControl() {
|
|||
}
|
||||
}
|
||||
|
||||
var inputs = document.querySelectorAll("input[type=text], input[type=number], textarea");
|
||||
var inputs = document.querySelectorAll("input[type=text], input[type=password], input[type=number], textarea");
|
||||
for (var i = 0, length = inputs.length; i < length; i++) {
|
||||
inputs[i].addEventListener("focus", raiseKeyboard);
|
||||
inputs[i].addEventListener("blur", lowerKeyboard);
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
function loaded() {
|
||||
bindExploreButtons();
|
||||
}
|
||||
|
||||
function bindExploreButtons() {
|
||||
$('#exploreClaraMarketplace').on('click', function() {
|
||||
window.location = "https://clara.io/library?public=true"
|
||||
})
|
||||
$('#exploreHifiMarketplace').on('click', function() {
|
||||
window.location = "http://www.highfidelity.com/marketplace"
|
||||
})
|
||||
}
|
246
scripts/system/html/js/marketplacesInject.js
Normal file
246
scripts/system/html/js/marketplacesInject.js
Normal file
|
@ -0,0 +1,246 @@
|
|||
//
|
||||
// marketplacesInject.js
|
||||
//
|
||||
// Created by David Rowe on 12 Nov 2016.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Injected into marketplace Web pages.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
(function () {
|
||||
|
||||
// Event bridge messages.
|
||||
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
|
||||
var GOTO_DIRECTORY = "GOTO_DIRECTORY";
|
||||
var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS";
|
||||
var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS";
|
||||
var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS";
|
||||
|
||||
var canWriteAssets = false;
|
||||
|
||||
function injectCommonCode(isDirectoryPage) {
|
||||
|
||||
// Supporting styles from marketplaces.css.
|
||||
// Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain.
|
||||
$("head").append(
|
||||
'<style>' +
|
||||
'#marketplace-navigation { font-family: Arial, Helvetica, sans-serif; width: 100%; height: 50px; background: #00b4ef; position: fixed; bottom: 0; z-index: 1000; }' +
|
||||
'#marketplace-navigation .glyph { margin-left: 20px; margin-right: 3px; font-family: sans-serif; color: #fff; font-size: 24px; line-height: 50px; }' +
|
||||
'#marketplace-navigation .text { color: #fff; font-size: 18px; line-height: 50px; vertical-align: top; position: relative; top: 1px; }' +
|
||||
'#marketplace-navigation input#back-button { position: absolute; left: 20px; margin-top: 12px; padding-left: 0; padding-right: 5px; }' +
|
||||
'#marketplace-navigation input#all-markets { position: absolute; right: 20px; margin-top: 12px; padding-left: 15px; padding-right: 15px; }' +
|
||||
'#marketplace-navigation .right { position: absolute; right: 20px; }' +
|
||||
'</style>'
|
||||
);
|
||||
|
||||
// Supporting styles from edit-style.css.
|
||||
// Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain.
|
||||
$("head").append(
|
||||
'<style>' +
|
||||
'input[type=button] { font-family: Arial, Helvetica, sans-serif; font-weight: bold; font-size: 12px; text-transform: uppercase; vertical-align: center; height: 28px; min-width: 100px; padding: 0 15px; border-radius: 5px; border: none; color: #fff; background-color: #000; background: linear-gradient(#343434 20%, #000 100%); cursor: pointer; }' +
|
||||
'input[type=button].white { color: #121212; background-color: #afafaf; background: linear-gradient(#fff 20%, #afafaf 100%); }' +
|
||||
'input[type=button].white:enabled:hover { background: linear-gradient(#fff, #fff); border: none; }' +
|
||||
'input[type=button].white:active { background: linear-gradient(#afafaf, #afafaf); }' +
|
||||
'</style>'
|
||||
);
|
||||
|
||||
// Footer.
|
||||
var isInitialHiFiPage = location.href === "https://metaverse.highfidelity.com/marketplace?";
|
||||
$("body").append(
|
||||
'<div id="marketplace-navigation">' +
|
||||
(!isInitialHiFiPage ? '<input id="back-button" type="button" class="white" value="< Back" />' : '') +
|
||||
(isInitialHiFiPage ? '<span class="glyph">🛈</span> <span class="text">See also other marketplaces.</span>' : '') +
|
||||
(!isDirectoryPage ? '<input id="all-markets" type="button" class="white" value="See All Markets" />' : '') +
|
||||
(isDirectoryPage ? '<span class="right"><span class="glyph">🛈</span> <span class="text">Select a marketplace to explore.</span><span>' : '') +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
// Footer actions.
|
||||
$("#back-button").on("click", function () {
|
||||
window.history.back();
|
||||
});
|
||||
$("#all-markets").on("click", function () {
|
||||
EventBridge.emitWebEvent(GOTO_DIRECTORY);
|
||||
});
|
||||
}
|
||||
|
||||
function injectDirectoryCode() {
|
||||
|
||||
// Remove e-mail hyperlink.
|
||||
var letUsKnow = $("#letUsKnow");
|
||||
letUsKnow.replaceWith(letUsKnow.html());
|
||||
|
||||
// Add button links.
|
||||
$('#exploreClaraMarketplace').on('click', function () {
|
||||
window.location = "https://clara.io/library?gameCheck=true&public=true"
|
||||
});
|
||||
$('#exploreHifiMarketplace').on('click', function () {
|
||||
window.location = "http://www.highfidelity.com/marketplace"
|
||||
});
|
||||
}
|
||||
|
||||
function injectHiFiCode() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
function updateClaraCode() {
|
||||
// Have to repeatedly update Clara page because its content can change dynamically without location.href changing.
|
||||
|
||||
// Clara library page.
|
||||
if (location.href.indexOf("clara.io/library") !== -1) {
|
||||
// Make entries navigate to "Image" view instead of default "Real Time" view.
|
||||
var elements = $("a.thumbnail");
|
||||
for (var i = 0, length = elements.length; i < length; i++) {
|
||||
var value = elements[i].getAttribute("href");
|
||||
if (value.slice(-6) !== "/image") {
|
||||
elements[i].setAttribute("href", value + "/image");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clara item page.
|
||||
if (location.href.indexOf("clara.io/view/") !== -1) {
|
||||
// Make site navigation links retain gameCheck etc. parameters.
|
||||
var element = $("a[href^=\'/library\']")[0];
|
||||
var parameters = "?gameCheck=true&public=true";
|
||||
var href = element.getAttribute("href");
|
||||
if (href.slice(-parameters.length) !== parameters) {
|
||||
element.setAttribute("href", href + parameters);
|
||||
}
|
||||
|
||||
// Replace download options with a single, "Download to High Fidelity" option.
|
||||
var buttons = $("a.embed-button").parent("div");
|
||||
if (buttons.length > 0) {
|
||||
var downloadFBX = buttons.find("a[data-extension=\'fbx\']")[0];
|
||||
downloadFBX.addEventListener("click", startAutoDownload);
|
||||
var firstButton = buttons.children(":first-child")[0];
|
||||
buttons[0].insertBefore(downloadFBX, firstButton);
|
||||
downloadFBX.setAttribute("class", "btn btn-primary download");
|
||||
downloadFBX.innerHTML = "<i class=\'glyphicon glyphicon-download-alt\'></i> Download to High Fidelity";
|
||||
buttons.children(":nth-child(2), .btn-group , .embed-button").each(function () { this.remove(); });
|
||||
}
|
||||
|
||||
// Move the "Download to High Fidelity" button to be more visible on tablet.
|
||||
if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) {
|
||||
// Moving the button stops the Clara.io download from starting so instead, make a visual copy in the right place
|
||||
// and wire its click event to click the original button.
|
||||
var downloadContainer = $('<div id="hifi-download-container"></div>');
|
||||
$(".top-title .col-sm-4").append(downloadContainer);
|
||||
var downloadButton = $("a[data-extension=\'fbx\']").clone();
|
||||
downloadButton[0].addEventListener("click", function () { downloadFBX.click(); });
|
||||
downloadContainer.append(downloadButton);
|
||||
downloadFBX.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
// Automatic download to High Fidelity.
|
||||
var downloadTimer;
|
||||
function startAutoDownload(event) {
|
||||
if (!canWriteAssets) {
|
||||
console.log("Clara.io FBX file download cancelled because no permissions to write to Asset Server");
|
||||
EventBridge.emitWebEvent(WARN_USER_NO_PERMISSIONS);
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
window.scrollTo(0, 0); // Scroll to top ready for history.back().
|
||||
if (!downloadTimer) {
|
||||
downloadTimer = setInterval(autoDownload, 1000);
|
||||
}
|
||||
}
|
||||
function autoDownload() {
|
||||
if ($("div.download-body").length !== 0) {
|
||||
var downloadButton = $("div.download-body a.download-file");
|
||||
if (downloadButton.length > 0) {
|
||||
clearInterval(downloadTimer);
|
||||
downloadTimer = null;
|
||||
var href = downloadButton[0].href;
|
||||
EventBridge.emitWebEvent(CLARA_IO_DOWNLOAD + " " + href);
|
||||
console.log("Clara.io FBX file download initiated for " + href);
|
||||
$("a.btn.cancel").click();
|
||||
history.back(); // Remove history item created by clicking "download".
|
||||
};
|
||||
} else if ($("div#view-signup_login_dialog").length === 0) {
|
||||
// Don't stop checking for button if user is asked to log in.
|
||||
clearInterval(downloadTimer);
|
||||
downloadTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function injectClaraCode() {
|
||||
|
||||
// Make space for marketplaces footer in Clara pages.
|
||||
$("head").append(
|
||||
'<style>' +
|
||||
'#app { margin-bottom: 135px; }' +
|
||||
'.footer { bottom: 50px; }' +
|
||||
'</style>'
|
||||
);
|
||||
|
||||
// Condense space.
|
||||
$("head").append(
|
||||
'<style>' +
|
||||
'div.page-title { line-height: 1.2; font-size: 13px; }' +
|
||||
'div.page-title-row { padding-bottom: 0; }' +
|
||||
'</style>'
|
||||
);
|
||||
|
||||
// Move "Download to High Fidelity" button.
|
||||
$("head").append(
|
||||
'<style>' +
|
||||
'#hifi-download-container { position: absolute; top: 6px; right: 16px; }' +
|
||||
'</style>'
|
||||
);
|
||||
|
||||
// Update code injected per page displayed.
|
||||
var updateClaraCodeInterval = undefined;
|
||||
updateClaraCode();
|
||||
updateClaraCodeInterval = setInterval(function () {
|
||||
updateClaraCode();
|
||||
}, 1000);
|
||||
|
||||
window.addEventListener("unload", function () {
|
||||
clearInterval(updateClaraCodeInterval);
|
||||
updateClaraCodeInterval = undefined;
|
||||
});
|
||||
|
||||
EventBridge.emitWebEvent(QUERY_CAN_WRITE_ASSETS);
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
|
||||
EventBridge.scriptEventReceived.connect(function (message) {
|
||||
if (message.slice(0, CAN_WRITE_ASSETS.length) === CAN_WRITE_ASSETS) {
|
||||
canWriteAssets = message.slice(-4) === "true";
|
||||
}
|
||||
});
|
||||
|
||||
var DIRECTORY = 0;
|
||||
var HIFI = 1;
|
||||
var CLARA = 2;
|
||||
var pageType = DIRECTORY;
|
||||
|
||||
if (location.href.indexOf("highfidelity.com/") !== -1) { pageType = HIFI; }
|
||||
if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; }
|
||||
|
||||
injectCommonCode(pageType === DIRECTORY);
|
||||
switch (pageType) {
|
||||
case DIRECTORY:
|
||||
injectDirectoryCode();
|
||||
break;
|
||||
case HIFI:
|
||||
injectHiFiCode();
|
||||
break;
|
||||
case CLARA:
|
||||
injectClaraCode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load / unload.
|
||||
window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready().
|
||||
|
||||
}());
|
|
@ -13,19 +13,12 @@
|
|||
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/marketplaces.css">
|
||||
<script src="js/jquery-2.1.4.min.js"></script>
|
||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
||||
<script src="js/marketplaces.js"></script>
|
||||
</head>
|
||||
<body onload='loaded();'>
|
||||
<body>
|
||||
<div class="marketplaces-container">
|
||||
<h2 class="marketplaces-title">
|
||||
Marketplaces
|
||||
</h2>
|
||||
<h2 class="marketplaces-title">Marketplaces</h2>
|
||||
<div class="marketplaces-intro-text">
|
||||
<p>
|
||||
You can bring content into High Fidelity forom anywhere you want. Here are a few places that support direct import of content right now. If you'd like to suggest a Market to include here, <a href="mailto:contact@highfidelity.io">let us know.</a>
|
||||
</p>
|
||||
<p>You can bring content into High Fidelity from anywhere you want. Here are a few places that support direct import of content right now. If you'd like to suggest a Market to include here, <a id="letUsKnow" href="mailto:contact@highfidelity.io">let us know.</a></p>
|
||||
</div>
|
||||
<div class="marketplace-tile">
|
||||
<div class="marketplace-tile-first-column">
|
||||
|
@ -34,7 +27,7 @@
|
|||
<div class="marketplace-tile-second-column">
|
||||
<p class="marketplace-tile-description">This is the default High Fidelity marketplace. Viewing and downloading content from here is fully supported in Interface.</p>
|
||||
<div class="exploreButton-holder">
|
||||
<input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace"></input></div>
|
||||
<input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace" />
|
||||
</div>
|
||||
<hr class="tile-divider">
|
||||
</div>
|
||||
|
@ -45,21 +38,21 @@
|
|||
<div class="marketplace-tile-second-column">
|
||||
<p class="marketplace-tile-description">Clara.io has thousands of models available for importing into High Fidelity. Follow these steps for the best experience:</p>
|
||||
<ol class="marketplace-clara-steps">
|
||||
<li><a href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
|
||||
<li>Choose a model from the list and click Download -> Autodesk FBX.</li>
|
||||
<li>After the file processes, click Download.</li>
|
||||
<li>Add the model to your asset server, then find it from the list and choose Add To World.</li>
|
||||
<li><a id="claraSignUp" href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
|
||||
<li>Choose a model from the list and click “Download to High Fidelity”.</li>
|
||||
</ol>
|
||||
<div class="exploreButton-holder">
|
||||
<input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace"></input>
|
||||
<input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace" />
|
||||
</div>
|
||||
</div>
|
||||
<hr class="tile-divider">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<!-- Code similar to the following is injected; the following is included here to help with CSS styling.
|
||||
<div id="marketplace-navigation">
|
||||
<span class="glyph">🛈</span> <span class="text">Select a marketplace to explore.</span>
|
||||
<input id="all-markets" type="button" class="white" value="See All Markets" />
|
||||
</div>
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -11,7 +11,7 @@
|
|||
var RAD_TO_DEG = 180 / Math.PI;
|
||||
var X_AXIS = {x: 1, y: 0, z: 0};
|
||||
var Y_AXIS = {x: 0, y: 1, z: 0};
|
||||
var DEFAULT_DPI = 32;
|
||||
var DEFAULT_DPI = 30;
|
||||
var DEFAULT_WIDTH = 0.5;
|
||||
|
||||
var TABLET_URL = "https://s3.amazonaws.com/hifi-public/tony/tablet.fbx";
|
||||
|
@ -43,14 +43,16 @@ WebTablet = function (url, width, dpi, clientOnly) {
|
|||
|
||||
var ASPECT = 4.0 / 3.0;
|
||||
var WIDTH = width || DEFAULT_WIDTH;
|
||||
var HEIGHT = WIDTH * ASPECT;
|
||||
var TABLET_HEIGHT_SCALE = 640 / 680; // Screen size of tablet entity isn't quite the desired aspect.
|
||||
var HEIGHT = WIDTH * ASPECT * TABLET_HEIGHT_SCALE;
|
||||
var DEPTH = 0.025;
|
||||
var DPI = dpi || DEFAULT_DPI;
|
||||
var SENSOR_TO_ROOM_MATRIX = -2;
|
||||
|
||||
var spawnInfo = calcSpawnInfo();
|
||||
|
||||
var tabletEntityPosition = spawnInfo.position;
|
||||
var tabletEntityRotation = spawnInfo.rotation;
|
||||
|
||||
this.tabletEntityID = Entities.addEntity({
|
||||
name: "tablet",
|
||||
type: "Model",
|
||||
|
@ -62,40 +64,50 @@ WebTablet = function (url, width, dpi, clientOnly) {
|
|||
}),
|
||||
dimensions: {x: WIDTH, y: HEIGHT, z: DEPTH},
|
||||
parentID: MyAvatar.sessionUUID,
|
||||
parentJointIndex: -2
|
||||
parentJointIndex: SENSOR_TO_ROOM_MATRIX
|
||||
}, clientOnly);
|
||||
|
||||
var WEB_ENTITY_REDUCTION_FACTOR = {x: 0.78, y: 0.85};
|
||||
var WEB_ENTITY_Z_OFFSET = -0.01;
|
||||
var WEB_OVERLAY_Z_OFFSET = -0.01;
|
||||
|
||||
var webEntityRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS));
|
||||
var webEntityPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_ENTITY_Z_OFFSET, Quat.getFront(webEntityRotation)));
|
||||
var webOverlayRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS));
|
||||
var webOverlayPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_OVERLAY_Z_OFFSET, Quat.getFront(webOverlayRotation)));
|
||||
|
||||
this.webEntityID = Entities.addEntity({
|
||||
name: "web",
|
||||
type: "Web",
|
||||
sourceUrl: url,
|
||||
dimensions: {x: WIDTH * WEB_ENTITY_REDUCTION_FACTOR.x,
|
||||
y: HEIGHT * WEB_ENTITY_REDUCTION_FACTOR.y,
|
||||
z: 0.1},
|
||||
position: webEntityPosition,
|
||||
rotation: webEntityRotation,
|
||||
shapeType: "box",
|
||||
this.webOverlayID = Overlays.addOverlay("web3d", {
|
||||
url: url,
|
||||
position: webOverlayPosition,
|
||||
rotation: webOverlayRotation,
|
||||
resolution: { x: 480, y: 640 },
|
||||
dpi: DPI,
|
||||
color: { red: 255, green: 255, blue: 255 },
|
||||
alpha: 1.0,
|
||||
parentID: this.tabletEntityID,
|
||||
parentJointIndex: -1
|
||||
}, clientOnly);
|
||||
});
|
||||
|
||||
this.state = "idle";
|
||||
};
|
||||
|
||||
WebTablet.prototype.setURL = function (url) {
|
||||
Overlays.editOverlay(this.webOverlayID, { url: url });
|
||||
};
|
||||
|
||||
WebTablet.prototype.setScriptURL = function (scriptURL) {
|
||||
Overlays.editOverlay(this.webOverlayID, { scriptURL: scriptURL });
|
||||
};
|
||||
|
||||
WebTablet.prototype.getOverlayObject = function () {
|
||||
return Overlays.getOverlayObject(this.webOverlayID);
|
||||
};
|
||||
|
||||
WebTablet.prototype.destroy = function () {
|
||||
Entities.deleteEntity(this.webEntityID);
|
||||
Overlays.deleteOverlay(this.webOverlayID);
|
||||
Entities.deleteEntity(this.tabletEntityID);
|
||||
};
|
||||
|
||||
WebTablet.prototype.pickle = function () {
|
||||
return JSON.stringify({webEntityID: this.webEntityID, tabletEntityID: this.tabletEntityID});
|
||||
return JSON.stringify({ webOverlayID: this.webOverlayID, tabletEntityID: this.tabletEntityID });
|
||||
};
|
||||
|
||||
WebTablet.unpickle = function (string) {
|
||||
if (!string) {
|
||||
return;
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
//
|
||||
// clara.js
|
||||
//
|
||||
// Created by Eric Levin on 8 Jan 2016
|
||||
// Edited by Elisa Lupin-Jimenez on 23 Aug 2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
(function() { // BEGIN LOCAL_SCOPE
|
||||
|
||||
var toolIconUrl = Script.resolvePath("../assets/images/tools/");
|
||||
var qml = Script.resolvePath("../../../resources/qml/Marketplaces.qml")
|
||||
|
||||
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
|
||||
|
||||
var marketplaceWindow = new OverlayWindow({
|
||||
title: "Marketplace",
|
||||
source: qml,
|
||||
width: 1000,
|
||||
height: 900,
|
||||
toolWindow: false,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
var toolHeight = 50;
|
||||
var toolWidth = 50;
|
||||
var TOOLBAR_MARGIN_Y = 0;
|
||||
|
||||
|
||||
function showMarketplace(marketplaceID) {
|
||||
var url = MARKETPLACE_URL;
|
||||
if (marketplaceID) {
|
||||
url = url + "/items/" + marketplaceID;
|
||||
}
|
||||
marketplaceWindow.setVisible(true);
|
||||
|
||||
UserActivityLogger.openedMarketplace();
|
||||
}
|
||||
|
||||
function hideMarketplace() {
|
||||
marketplaceWindow.setVisible(false);
|
||||
}
|
||||
|
||||
function toggleMarketplace() {
|
||||
if (marketplaceWindow.visible) {
|
||||
hideMarketplace();
|
||||
} else {
|
||||
showMarketplace();
|
||||
}
|
||||
}
|
||||
|
||||
var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
|
||||
|
||||
var browseExamplesButton = toolBar.addButton({
|
||||
imageURL: toolIconUrl + "market.svg",
|
||||
objectName: "marketplace",
|
||||
buttonState: 1,
|
||||
defaultState: 1,
|
||||
hoverState: 3,
|
||||
alpha: 0.9
|
||||
});
|
||||
|
||||
function onExamplesWindowVisibilityChanged() {
|
||||
browseExamplesButton.writeProperty('buttonState', marketplaceWindow.visible ? 0 : 1);
|
||||
browseExamplesButton.writeProperty('defaultState', marketplaceWindow.visible ? 0 : 1);
|
||||
browseExamplesButton.writeProperty('hoverState', marketplaceWindow.visible ? 2 : 3);
|
||||
}
|
||||
function onClick() {
|
||||
toggleMarketplace();
|
||||
}
|
||||
browseExamplesButton.clicked.connect(onClick);
|
||||
marketplaceWindow.visibleChanged.connect(onExamplesWindowVisibilityChanged);
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
toolBar.removeButton("marketplace");
|
||||
browseExamplesButton.clicked.disconnect(onClick);
|
||||
marketplaceWindow.visibleChanged.disconnect(onExamplesWindowVisibilityChanged);
|
||||
});
|
||||
|
||||
}()); // END LOCAL_SCOPE
|
178
scripts/system/marketplaces/marketplaces.js
Normal file
178
scripts/system/marketplaces/marketplaces.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// marketplaces.js
|
||||
//
|
||||
// Created by Eric Levin on 8 Jan 2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
(function() { // BEGIN LOCAL_SCOPE
|
||||
|
||||
/* global WebTablet */
|
||||
Script.include("../libraries/WebTablet.js");
|
||||
|
||||
var toolIconUrl = Script.resolvePath("../assets/images/tools/");
|
||||
|
||||
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
|
||||
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page.
|
||||
var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
|
||||
var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
|
||||
|
||||
// Event bridge messages.
|
||||
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
|
||||
var GOTO_DIRECTORY = "GOTO_DIRECTORY";
|
||||
var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS";
|
||||
var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS";
|
||||
var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS";
|
||||
var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server.";
|
||||
|
||||
var marketplaceWindow = new OverlayWebWindow({
|
||||
title: "Marketplace",
|
||||
source: "about:blank",
|
||||
width: 900,
|
||||
height: 700,
|
||||
visible: false
|
||||
});
|
||||
marketplaceWindow.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL);
|
||||
marketplaceWindow.webEventReceived.connect(function (message) {
|
||||
if (message === GOTO_DIRECTORY) {
|
||||
marketplaceWindow.setURL(MARKETPLACES_URL);
|
||||
}
|
||||
if (message === QUERY_CAN_WRITE_ASSETS) {
|
||||
marketplaceWindow.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets());
|
||||
}
|
||||
if (message === WARN_USER_NO_PERMISSIONS) {
|
||||
Window.alert(NO_PERMISSIONS_ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
var toolHeight = 50;
|
||||
var toolWidth = 50;
|
||||
var TOOLBAR_MARGIN_Y = 0;
|
||||
var marketplaceVisible = false;
|
||||
var marketplaceWebTablet;
|
||||
|
||||
// We persist clientOnly data in the .ini file, and reconstitute it on restart.
|
||||
// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change.
|
||||
var persistenceKey = "io.highfidelity.lastDomainTablet";
|
||||
|
||||
function shouldShowWebTablet() {
|
||||
var rightPose = Controller.getPoseValue(Controller.Standard.RightHand);
|
||||
var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand);
|
||||
var hasHydra = !!Controller.Hardware.Hydra;
|
||||
return HMD.active && (leftPose.valid || rightPose.valid || hasHydra);
|
||||
}
|
||||
|
||||
function showMarketplace() {
|
||||
if (shouldShowWebTablet()) {
|
||||
updateButtonState(true);
|
||||
marketplaceWebTablet = new WebTablet(MARKETPLACE_URL_INITIAL, null, null, true);
|
||||
Settings.setValue(persistenceKey, marketplaceWebTablet.pickle());
|
||||
marketplaceWebTablet.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL);
|
||||
marketplaceWebTablet.getOverlayObject().webEventReceived.connect(function (message) {
|
||||
if (message === GOTO_DIRECTORY) {
|
||||
marketplaceWebTablet.setURL(MARKETPLACES_URL);
|
||||
}
|
||||
if (message === QUERY_CAN_WRITE_ASSETS) {
|
||||
marketplaceWebTablet.getOverlayObject().emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets());
|
||||
}
|
||||
if (message === WARN_USER_NO_PERMISSIONS) {
|
||||
Window.alert(NO_PERMISSIONS_ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
marketplaceWindow.setURL(MARKETPLACE_URL_INITIAL);
|
||||
marketplaceWindow.setVisible(true);
|
||||
}
|
||||
|
||||
marketplaceVisible = true;
|
||||
UserActivityLogger.openedMarketplace();
|
||||
}
|
||||
|
||||
function hideTablet(tablet) {
|
||||
if (!tablet) {
|
||||
return;
|
||||
}
|
||||
updateButtonState(false);
|
||||
tablet.destroy();
|
||||
marketplaceWebTablet = null;
|
||||
Settings.setValue(persistenceKey, "");
|
||||
}
|
||||
function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated
|
||||
var tablet = WebTablet.unpickle(Settings.getValue(persistenceKey, ""));
|
||||
hideTablet(tablet);
|
||||
}
|
||||
function hideMarketplace() {
|
||||
if (marketplaceWindow.visible) {
|
||||
marketplaceWindow.setVisible(false);
|
||||
marketplaceWindow.setURL("about:blank");
|
||||
} else if (marketplaceWebTablet) {
|
||||
hideTablet(marketplaceWebTablet);
|
||||
}
|
||||
marketplaceVisible = false;
|
||||
}
|
||||
marketplaceWindow.closed.connect(function () {
|
||||
marketplaceWindow.setURL("about:blank");
|
||||
});
|
||||
|
||||
function toggleMarketplace() {
|
||||
if (marketplaceVisible) {
|
||||
hideMarketplace();
|
||||
} else {
|
||||
showMarketplace();
|
||||
}
|
||||
}
|
||||
|
||||
var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
|
||||
|
||||
var browseExamplesButton = toolBar.addButton({
|
||||
imageURL: toolIconUrl + "market.svg",
|
||||
objectName: "marketplace",
|
||||
buttonState: 1,
|
||||
defaultState: 1,
|
||||
hoverState: 3,
|
||||
alpha: 0.9
|
||||
});
|
||||
|
||||
function updateButtonState(visible) {
|
||||
browseExamplesButton.writeProperty('buttonState', visible ? 0 : 1);
|
||||
browseExamplesButton.writeProperty('defaultState', visible ? 0 : 1);
|
||||
browseExamplesButton.writeProperty('hoverState', visible ? 2 : 3);
|
||||
}
|
||||
function onMarketplaceWindowVisibilityChanged() {
|
||||
updateButtonState(marketplaceWindow.visible);
|
||||
marketplaceVisible = marketplaceWindow.visible;
|
||||
}
|
||||
|
||||
function onCanWriteAssetsChanged() {
|
||||
var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets();
|
||||
if (marketplaceWindow.visible) {
|
||||
marketplaceWindow.emitScriptEvent(message);
|
||||
}
|
||||
if (marketplaceWebTablet) {
|
||||
marketplaceWebTablet.getOverlayObject().emitScriptEvent(message);
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
toggleMarketplace();
|
||||
}
|
||||
|
||||
browseExamplesButton.clicked.connect(onClick);
|
||||
marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged);
|
||||
Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged);
|
||||
|
||||
clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash.
|
||||
// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}),
|
||||
// but the HUD version stays around, so lets do the same.
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
toolBar.removeButton("marketplace");
|
||||
browseExamplesButton.clicked.disconnect(onClick);
|
||||
marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged);
|
||||
Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged);
|
||||
});
|
||||
|
||||
}()); // END LOCAL_SCOPE
|
Loading…
Reference in a new issue