Merge pull request #645 from keeshii/feature/qml_entities

Added support for QML inside web-entities.
This commit is contained in:
ksuprynowicz 2023-10-19 22:46:07 +02:00 committed by GitHub
commit 79cf24d30f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 980 additions and 82 deletions

View file

@ -9,68 +9,94 @@
//
import QtQuick 2.5
import QtGraphicalEffects 1.0
Item {
id: root
anchors.fill: parent
property string url: ""
property string scriptUrl: null
property bool useBackground: true
property string userAgent: ""
property string url
RadialGradient {
onUrlChanged: {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
onScriptUrlChanged: {
if (loader.item) {
if (root.webViewLoaded) {
loader.item.scriptUrl = root.scriptUrl;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
onUseBackgroundChanged: {
if (loader.item) {
if (root.webViewLoaded) {
loader.item.useBackground = root.useBackground;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
onUserAgentChanged: {
if (loader.item) {
if (root.webViewLoaded) {
loader.item.userAgent = root.userAgent;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
// Handle message traffic from our loaded QML to the script that launched us
onItemChanged: {
if (loader.item && loader.item.sendToScript) {
loader.item.sendToScript.connect(sendToScript);
}
}
property var item: null
property bool webViewLoaded: false
// Handle message traffic from the script that launched us to the loaded QML
function fromScript(message) {
if (loader.item && loader.item.fromScript) {
loader.item.fromScript(message);
}
}
Loader {
id: loader
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#262626" }
GradientStop { position: 1.0; color: "#000000" }
}
function load(url, scriptUrl, useBackground, userAgent) {
// Ensure we reset any existing item to "about:blank" to ensure web audio stops: DEV-2375
if (loader.item && root.webViewLoaded) {
loader.item.url = "about:blank"
}
if (url.match(/\.qml$/)) {
root.webViewLoaded = false;
loader.setSource(url);
} else {
root.webViewLoaded = true;
loader.setSource("./Web3DSurfaceAndroid.qml", {
url: url,
scriptUrl: scriptUrl,
useBackground: useBackground,
userAgent: userAgent
});
}
}
function shortUrl(url) {
var hostBegin = url.indexOf("://");
if (hostBegin > -1) {
url = url.substring(hostBegin + 3);
}
var portBegin = url.indexOf(":");
if (portBegin > -1) {
url = url.substring(0, portBegin);
}
var pathBegin = url.indexOf("/");
if (pathBegin > -1) {
url = url.substring(0, pathBegin);
}
if (url.length > 45) {
url = url.substring(0, 45);
}
return url;
Component.onCompleted: {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
Text {
id: urlText
text: shortUrl(url)
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.fill: parent
anchors.rightMargin: 10
anchors.leftMargin: 10
font.family: "Cairo"
font.weight: Font.DemiBold
font.pointSize: 48
fontSizeMode: Text.Fit
color: "#FFFFFF"
minimumPixelSize: 5
}
Image {
id: hand
source: "../../icons/hand.svg"
width: 300
height: 300
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: 100
anchors.rightMargin: 100
}
signal sendToScript(var message);
}

View file

@ -0,0 +1,82 @@
//
// Web3DSurface.qml
//
// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018
// 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 QtGraphicalEffects 1.0
Item {
property string url
property string scriptUrl
property bool useBackground
property string userAgent
RadialGradient {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#262626" }
GradientStop { position: 1.0; color: "#000000" }
}
}
function destroy() { }
function shortUrl(url) {
var hostBegin = url.indexOf("://");
if (hostBegin > -1) {
url = url.substring(hostBegin + 3);
}
var portBegin = url.indexOf(":");
if (portBegin > -1) {
url = url.substring(0, portBegin);
}
var pathBegin = url.indexOf("/");
if (pathBegin > -1) {
url = url.substring(0, pathBegin);
}
if (url.length > 45) {
url = url.substring(0, 45);
}
return url;
}
Text {
id: urlText
text: shortUrl(url)
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.fill: parent
anchors.rightMargin: 10
anchors.leftMargin: 10
font.family: "Cairo"
font.weight: Font.DemiBold
font.pointSize: 48
fontSizeMode: Text.Fit
color: "#FFFFFF"
minimumPixelSize: 5
}
Image {
id: hand
source: "../../icons/hand.svg"
width: 300
height: 300
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: 100
anchors.rightMargin: 100
}
}

View file

@ -10,8 +10,8 @@
//
import QtQuick 2.5
import "controls" as Controls
import controlsUit 1.0 as Controls
import "controls"
Item {
id: root
@ -26,45 +26,78 @@ Item {
}
onScriptUrlChanged: {
if (root.item) {
root.item.scriptUrl = root.scriptUrl;
if (loader.item) {
if (root.webViewLoaded) {
loader.item.scriptUrl = root.scriptUrl;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
onUseBackgroundChanged: {
if (root.item) {
root.item.useBackground = root.useBackground;
if (loader.item) {
if (root.webViewLoaded) {
loader.item.useBackground = root.useBackground;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
onUserAgentChanged: {
if (root.item) {
root.item.userAgent = root.userAgent;
if (loader.item) {
if (root.webViewLoaded) {
loader.item.userAgent = root.userAgent;
}
} else {
load(root.url, root.scriptUrl, root.useBackground, root.userAgent);
}
}
// Handle message traffic from our loaded QML to the script that launched us
onItemChanged: {
if (loader.item && loader.item.sendToScript) {
loader.item.sendToScript.connect(sendToScript);
}
}
property var item: null
property bool webViewLoaded: false
// Handle message traffic from the script that launched us to the loaded QML
function fromScript(message) {
if (loader.item && loader.item.fromScript) {
loader.item.fromScript(message);
}
}
Loader {
id: loader
anchors.fill: parent
}
function load(url, scriptUrl, useBackground, userAgent) {
// Ensure we reset any existing item to "about:blank" to ensure web audio stops: DEV-2375
if (root.item != null) {
root.item.url = "about:blank"
root.item.destroy()
root.item = null
if (loader.item && root.webViewLoaded) {
if (root.webViewLoaded) {
loader.item.url = "about:blank"
}
loader.setSource(undefined);
}
if (url.match(/\.qml$/)) {
root.webViewLoaded = false;
loader.setSource(url);
} else {
root.webViewLoaded = true;
loader.setSource("./controls/WebView.qml", {
url: url,
scriptUrl: scriptUrl,
useBackground: useBackground,
userAgent: userAgent
});
}
QmlSurface.load("./controls/WebView.qml", root, function(newItem) {
root.item = newItem
root.item.url = url
root.item.scriptUrl = scriptUrl
root.item.useBackground = useBackground
root.item.userAgent = userAgent
})
}
Component.onCompleted: {

View file

@ -13,6 +13,8 @@ Rectangle {
property string url: "";
property bool canGoBack: false
property bool canGoForward: false
property bool useBackground: false
property string userAgent: ""
property string icon: ""
property var profile: {}

View file

@ -0,0 +1,599 @@
//
// Wizard.qml
//
// Created by keeshii on 26 Sep 2023
// Copyright 2023 Overte, Org.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import Hifi 1.0 as Hifi
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.12
import stylesUit 1.0 as HifiStylesUit
import controlsUit 1.0 as HifiControls
import "qrc:////qml//styles" as HifiStyles
import "qrc:////qml//hifi" as Hifi
Rectangle {
id: wizard
color: "#433952"
property int performancePreset: 0
property int refreshRateProfile: 0
property string displayName: ""
property bool keyboardEnabled: false
property bool punctuationMode: false
property bool keyboardRaised: false
function setStep(stepNum) {
stepList.completed = stepNum
switch (stepNum) {
case 0:
loader.sourceComponent = step1;
break;
case 1:
loader.sourceComponent = step2;
break;
case 2:
loader.sourceComponent = step3;
break;
case 3:
loader.sourceComponent = step4;
break;
case 4:
loader.sourceComponent = step5;
break;
default:
loader.setSource(undefined);
}
}
function completeWizard() {
var completionMessage = {
command: "complete-wizard",
data: {
performancePreset: wizard.performancePreset,
refreshRateProfile: wizard.refreshRateProfile,
displayName: wizard.displayName
}
};
eventBridge.emitWebEvent(JSON.stringify(completionMessage));
}
function handleWebEvent(message) {
var messageJSON = JSON.parse(message);
if (messageJSON.command === "script-to-web-initialize") {
wizard.performancePreset = messageJSON.data.performancePreset;
wizard.refreshRateProfile = messageJSON.data.refreshRateProfile;
wizard.displayName = messageJSON.data.displayName;
}
}
function initializeWizard() {
var initializeCommand = {"command": "first-run-wizard-ready"};
eventBridge.emitWebEvent(JSON.stringify(initializeCommand));
}
function stop() {
wizard.keyboardEnabled = false;
}
// Layout constants constants
HifiStyles.HifiConstants { id: hifi }
Rectangle {
id: steps
color: "#26202e"
width: parent.width - 8 * hifi.layout.spacing
height: hifi.layout.rowHeight + 6 * hifi.layout.spacing
anchors.top: wizard.top
anchors.topMargin: 4 * hifi.layout.spacing
anchors.horizontalCenter: wizard.horizontalCenter
radius: hifi.layout.spacing
ListView {
id: stepList
anchors.fill: parent
orientation: ListView.Horizontal
property int completed: 0
delegate: Item {
id: stepItem
width: stepList.width / 5
height: stepList.height
property int num: index
Rectangle {
width: parent.width
height: 1
anchors.left: stepCircle.horizontalCenter
anchors.top: stepCircle.verticalCenter
visible: stepItem.num + 1 < stepList.model.count
color: stepList.completed > stepItem.num ? "#4bb543" : "gray"
}
Rectangle {
id: stepCircle
color: stepList.completed > stepItem.num ? "#4bb543"
: (stepList.completed === stepItem.num ? "white" : "gray")
width: hifi.layout.rowHeight
height: hifi.layout.rowHeight
radius: hifi.layout.rowHeight / 2
anchors.top: stepItem.top
anchors.topMargin: hifi.layout.spacing
anchors.horizontalCenter: stepItem.horizontalCenter
Text {
id: stepNum
text: String(stepItem.num + 1)
color: stepList.completed === stepItem.num ? hifi.colors.text : "white"
anchors.centerIn: stepCircle
}
}
Text {
id: stepName
text: name
color: "white"
anchors.top: stepCircle.bottom
anchors.topMargin: hifi.layout.spacing
anchors.horizontalCenter: stepItem.horizontalCenter
font.bold: true
}
}
model: ListModel {
ListElement { name: "Welcome" }
ListElement { name: "Quality" }
ListElement { name: "Performance" }
ListElement { name: "Display Name" }
ListElement { name: "Completion" }
}
}
}
Loader {
id: loader
width: parent.width - 8 * hifi.layout.spacing
height: parent.height - steps.height - backButton.height - 12 * hifi.layout.spacing
anchors.top: steps.bottom
anchors.topMargin: 2 * hifi.layout.spacing
anchors.horizontalCenter: wizard.horizontalCenter
sourceComponent: step1
}
Component {
id: step1
Item {
id: step1Body
anchors.fill: loader
Text {
id: step1Header
width: parent.width
text: "Welcome to Overte!"
color: "white"
font.pixelSize: hifi.fonts.headerPixelSize
font.bold: true
anchors.top: step1Body.top
anchors.topMargin: 2 * hifi.layout.spacing
}
Text {
id: step1Text1
width: parent.width
text:
"Let's get you setup to experience the virtual world.<br />" +
"First, we need to select some performance and graphics quality options.<br />" +
"<br />" +
"Press <b><font color=\"#2e89ff\">Continue</font></b> when you are ready."
color: "white"
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
anchors.top: step1Header.bottom
anchors.topMargin: 2 * hifi.layout.spacing
}
}
}
Component {
id: step2
Item {
id: step2Body
anchors.fill: loader
Text {
id: step2Header
width: parent.width
text: "Quality"
color: "white"
font.pixelSize: hifi.fonts.headerPixelSize
font.bold: true
anchors.top: step2Body.top
anchors.topMargin: 2 * hifi.layout.spacing
}
Text {
id: step2Text1
width: parent.width
text:
"What level of visual quality would you like?<br />" +
"<b>Remember! If you do not have a powerful computer,<br />" +
"you may want to set this to low or medium at most.</b>"
color: "white"
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
anchors.top: step2Header.bottom
anchors.topMargin: 2 * hifi.layout.spacing
}
ColumnLayout {
anchors.top: step2Text1.bottom
anchors.topMargin: 2 * hifi.layout.spacing
RadioButton {
text:
"<font size=\"4\" color=\"#ff9900\"><b>Very Low Quality</b></font>\n" +
"<font color=\"white\">Slow Laptop / Very Slow Computer</font>"
onClicked: wizard.performancePreset = 1
checked: wizard.performancePreset === 1
}
RadioButton {
text:
"<font size=\"4\" color=\"#ffff00\"><b>Low Quality</b></font>\n" +
"<font color=\"white\">Average Laptop / Slow Computer</font>"
onClicked: wizard.performancePreset = 2
checked: wizard.performancePreset === 2
}
RadioButton {
text:
"<font size=\"4\" color=\"#00ba1c\"><b>Medium Quality</b></font>\n" +
"<font color=\"white\">Average Computer - </font><font color=\"#00ba1c\"><i>Recommended</i></font>"
onClicked: wizard.performancePreset = 3
checked: wizard.performancePreset === 3
}
RadioButton {
text:
"<font size=\"4\" color=\"#0096db\"><b>High Quality</b></font>\n" +
"<font color=\"white\">Gaming Computer</font>"
onClicked: wizard.performancePreset = 4
checked: wizard.performancePreset === 4
}
}
}
}
Component {
id: step3
Item {
id: step3Body
anchors.fill: loader
Text {
id: step3Header
width: parent.width
text: "Performance"
color: "white"
font.pixelSize: hifi.fonts.headerPixelSize
font.bold: true
anchors.top: step3Body.top
anchors.topMargin: 2 * hifi.layout.spacing
}
Text {
id: step3Text1
width: parent.width
text:
"Do you want a smooth experience <i>(high refresh rate)</i><br />" +
"or do you want to conserve power and resources <i>(low refresh rate)</i> on your computer?<br />" +
"<b><i>Note: This does not apply to virtual reality headsets.</i><b>"
color: "white"
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
anchors.top: step3Header.bottom
anchors.topMargin: 2 * hifi.layout.spacing
}
ColumnLayout {
anchors.top: step3Text1.bottom
anchors.topMargin: 2 * hifi.layout.spacing
RadioButton {
text:
"<font size=\"4\" color=\"#ff9900\"><b>Not Smooth (20 Hz)</b></font>\n" +
"<font color=\"white\">Conserve Power</font>"
onClicked: wizard.refreshRateProfile = 1
checked: wizard.refreshRateProfile === 1
}
RadioButton {
text:
"<font size=\"4\" color=\"#ffff00\"><b>Smooth (30 Hz)</b></font>\n" +
"<font color=\"white\">Use Average Resources</font>"
onClicked: wizard.refreshRateProfile = 2
checked: wizard.refreshRateProfile === 2
}
RadioButton {
text:
"<font size=\"4\" color=\"#00ba1c\"><b>Very Smooth (60 Hz)</b></font>\n" +
"<font color=\"white\">Use Maximum Resources - </font><font color=\"#00ba1c\"><i>Recommended</i></font>"
onClicked: wizard.refreshRateProfile = 3
checked: wizard.refreshRateProfile === 3
}
}
}
}
Component {
id: step4
Item {
id: step4Body
anchors.fill: loader
Text {
id: step4Header
width: parent.width
text: "Display Name"
color: "white"
font.pixelSize: hifi.fonts.headerPixelSize
font.bold: true
anchors.top: step4Body.top
anchors.topMargin: 2 * hifi.layout.spacing
}
Text {
id: step4Text1
width: parent.width
text:
"What should people call you?<br />" +
"This is simply a nickname, it will be shown in place of your username (if you have one)."
color: "white"
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
anchors.top: step4Header.bottom
anchors.topMargin: 2 * hifi.layout.spacing
}
Rectangle {
id: inputBar
width: parent.width
height: 40
color: 'white'
anchors.top: step4Text1.bottom
anchors.topMargin: 2 * hifi.layout.spacing
TextField {
id: displayName
text: wizard.displayName
focus: true
width: inputBar.width - inputBar.anchors.leftMargin - inputBar.anchors.rightMargin;
anchors {
left: inputBar.left;
leftMargin: 8;
verticalCenter: inputBar.verticalCenter;
}
onTextChanged: wizard.displayName = text
placeholderText: "Enter display name"
verticalAlignment: TextInput.AlignBottom
onAccepted: {
if (HMD.active) {
wizard.keyboardEnabled = false;
}
}
font {
family: hifi.fonts.fontFamily
pixelSize: hifi.fonts.pixelSize * 0.75
}
color: hifi.colors.text
background: Item {}
}
MouseArea {
anchors.fill: parent;
onClicked: {
displayName.focus = true;
displayName.forceActiveFocus();
if (HMD.active) {
wizard.keyboardEnabled = true;
}
}
}
}
}
}
Component {
id: step5
Item {
id: step5Body
anchors.fill: loader
Text {
id: step5Header
width: parent.width
text: "All done!"
color: "white"
font.pixelSize: hifi.fonts.headerPixelSize
font.bold: true
anchors.top: step5Body.top
anchors.topMargin: 2 * hifi.layout.spacing
}
Text {
id: step5Text1
width: parent.width
text:
"Now you're almost ready to go!<br />" +
"Press <font color=\"#1ee62e\">Complete</font> to save your setup.<br />" +
"Then take a look at the other information kiosks after completing this wizard."
color: "white"
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
anchors.top: step5Header.bottom
anchors.topMargin: 2 * hifi.layout.spacing
}
}
}
Button {
id: backButton
text: "< Back"
width: nextButton.width
anchors.bottom: wizard.bottom
anchors.left: wizard.left
anchors.bottomMargin: 4 * hifi.layout.spacing
anchors.leftMargin: 4 * hifi.layout.spacing
visible: stepList.completed > 0
onClicked: setStep(stepList.completed - 1)
contentItem: Text {
text: backButton.text
font.bold: true
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
implicitWidth: 100
implicitHeight: 40
gradient: Gradient {
GradientStop { position: 0 ; color: backButton.hovered ? "#0599fc" : "#0599fc" }
GradientStop { position: 1 ; color: backButton.hovered ? "#003670" : "#002259" }
}
border.color: "#26282a"
border.width: 1
radius: 4
}
}
Button {
id: nextButton
text: "Continue >"
anchors.bottom: wizard.bottom
anchors.right: wizard.right
anchors.bottomMargin: 4 * hifi.layout.spacing
anchors.rightMargin: 4 * hifi.layout.spacing
visible: stepList.completed < 4
onClicked: setStep(stepList.completed + 1)
rightPadding: 2 * hifi.layout.spacing
leftPadding: 2 * hifi.layout.spacing
contentItem: Text {
text: nextButton.text
font.bold: true
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
implicitWidth: 100
implicitHeight: 40
gradient: Gradient {
GradientStop { position: 0 ; color: nextButton.hovered ? "#0599fc" : "#0599fc" }
GradientStop { position: 1 ; color: nextButton.hovered ? "#003670" : "#002259" }
}
border.color: "#26282a"
border.width: 1
radius: 4
}
}
Button {
id: completeButton
text: "Complete"
width: nextButton.width
anchors.bottom: wizard.bottom
anchors.right: wizard.right
anchors.bottomMargin: 4 * hifi.layout.spacing
anchors.rightMargin: 4 * hifi.layout.spacing
visible: stepList.completed === 4
onClicked: completeWizard()
contentItem: Text {
text: completeButton.text
font.bold: true
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
implicitWidth: 100
implicitHeight: 40
gradient: Gradient {
GradientStop { position: 0 ; color: nextButton.hovered ? "#59ffc2" : "#00ff00" }
GradientStop { position: 1 ; color: nextButton.hovered ? "#196144" : "#003600" }
}
border.color: "#26282a"
border.width: 1
radius: 4
}
}
HifiControls.Keyboard {
id: keyboard
raised: parent.keyboardEnabled && parent.keyboardRaised
numeric: parent.punctuationMode
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
// Wait for the client-entity script to load before sending events
Timer {
id: timer
function setTimeout(cb, delayTime) {
timer.interval = delayTime;
timer.repeat = false;
timer.triggered.connect(cb);
timer.triggered.connect(function release () {
timer.triggered.disconnect(cb); // This is important
timer.triggered.disconnect(release); // This is important as well
});
timer.start();
}
}
Component.onCompleted: {
eventBridge.scriptEventReceived.connect(handleWebEvent);
timer.setTimeout(function(){ initializeWizard(); }, 2000);
}
Component.onDestruction: {
stop();
}
signal sendToScript(var message);
}

View file

@ -13,7 +13,7 @@
//
(function() {
var CONFIG_WIZARD_URL = "https://more.overte.org/tutorial/wizard.html?v=" + Math.floor(Math.random() * 65000);
var CONFIG_WIZARD_URL = "qrc:///serverless/Scripts/Wizard.qml";
var loaderEntityID;
var configWizardEntityID;
@ -54,7 +54,7 @@
"parentID": loaderEntityID,
"sourceUrl": CONFIG_WIZARD_URL,
"maxFPS": 60,
"dpi": 19,
"dpi": 15,
"useBackground": true,
"grab": {
"grabbable": false

View file

@ -3226,7 +3226,7 @@ void Application::initializeUi() {
return true;
} else {
for (const auto& str : safeURLS) {
if (!str.isEmpty() && str.endsWith(".qml") && url.toString().endsWith(".qml") &&
if (!str.isEmpty() && url.toString().endsWith(".qml") &&
url.toString().startsWith(str)) {
qCDebug(interfaceapp) << "Found matching url!" << url.host();
return true;

View file

@ -66,6 +66,10 @@ WebEntityRenderer::ContentType WebEntityRenderer::getContentType(const QString&
const QUrl url(urlString);
auto scheme = url.scheme();
if (urlString.toLower().endsWith(".qml")) {
return ContentType::QmlContent;
}
if (scheme == HIFI_URL_SCHEME_ABOUT || scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS ||
scheme == URL_SCHEME_DATA ||
urlString.toLower().endsWith(".htm") || urlString.toLower().endsWith(".html")) {

View file

@ -0,0 +1,149 @@
"use strict";
//
// androidControls.js
//
// Created by keeshii on September 26th, 2023.
// Copyright 2022-2023 Overte e.V.
//
// This script read touch screen events and triggers mouse events.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// SPDX-License-Identifier: Apache-2.0
//
(function () {
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
var DISPATCHER_TOUCH_PROPERTIES = ["id", "position", "rotation", "dimensions", "registrationPoint"];
var TAP_DELAY = 300;
function AndroidControls() {
this.onTouchStartFn = null;
this.onTouchEndFn = null;
this.touchStartTime = 0;
}
AndroidControls.prototype.intersectsOverlay = function (intersection) {
if (intersection && intersection.intersects && intersection.overlayID) {
return true;
}
return false;
};
AndroidControls.prototype.intersectsEntity = function (intersection) {
if (intersection && intersection.intersects && intersection.entityID) {
return true;
}
return false;
};
AndroidControls.prototype.findRayIntersection = function (pickRay) {
// Check 3D overlays and entities. Argument is an object with origin and direction.
var overlayRayIntersection = Overlays.findRayIntersection(pickRay);
var entityRayIntersection = Entities.findRayIntersection(pickRay, true);
var isOverlayInters = this.intersectsOverlay(overlayRayIntersection);
var isEntityInters = this.intersectsEntity(entityRayIntersection);
if (isOverlayInters && (!isEntityInters || overlayRayIntersection.distance < entityRayIntersection.distance)) {
return {type: 'overlay', obj: overlayRayIntersection};
} else if (isEntityInters) {
return {type: 'entity', obj: entityRayIntersection};
}
return false;
};
AndroidControls.prototype.createEventProperties = function (entityId, info, eventType) {
var pointerEvent = {
type: eventType,
id: 1,
pos2D: {x: 0, y: 0},
pos3D: info.obj.intersection,
normal: info.obj.surfaceNormal,
direction: info.obj.direction,
button: "Primary",
isPrimaryButton: true,
isLeftButton: true,
isPrimaryHeld: eventType === 'Press',
isSecondaryHeld: false,
isTertiaryHeld: false,
keyboardModifiers: 0
};
var properties = Entities.getEntityProperties(entityId, DISPATCHER_TOUCH_PROPERTIES);
if (properties.id === entityId) {
pointerEvent.pos2D = info.type === "entity"
? projectOntoEntityXYPlane(entityId, info.obj.intersection, properties)
: projectOntoOverlayXYPlane(entityId, info.obj.intersection, properties);
}
return pointerEvent;
};
AndroidControls.prototype.triggerClick = function (event) {
var info = this.findRayIntersection(Camera.computePickRay(event.x, event.y));
if (!info) {
return;
}
var entityId = info.type === "entity" ? info.obj.entityID : info.obj.overlayID;
var pressEvent = this.createEventProperties(entityId, info, 'Press');
var releaseEvent = this.createEventProperties(entityId, info, 'Release');
Entities.sendMousePressOnEntity(entityId, pressEvent);
Entities.sendClickDownOnEntity(entityId, pressEvent);
Script.setTimeout(function () {
Entities.sendMouseReleaseOnEntity(entityId, releaseEvent);
Entities.sendClickReleaseOnEntity(entityId, releaseEvent);
}, 75);
};
AndroidControls.prototype.onTouchStart = function (_event) {
this.touchStartTime = Date.now();
};
AndroidControls.prototype.onTouchEnd = function (event) {
var now = Date.now();
if (now - this.touchStartTime < TAP_DELAY) {
this.triggerClick(event);
}
this.touchStartTime = 0;
};
AndroidControls.prototype.init = function () {
var self = this;
this.onTouchStartFn = function (ev) {
self.onTouchStart(ev);
};
this.onTouchEndFn = function (ev) {
self.onTouchEnd(ev);
};
Controller.touchBeginEvent.connect(this.onTouchStartFn);
Controller.touchEndEvent.connect(this.onTouchEndFn);
};
AndroidControls.prototype.ending = function () {
if (this.onTouchStartFn) {
Controller.touchBeginEvent.disconnect(this.onTouchStartFn);
}
if (this.onTouchEndFn) {
Controller.touchEndEvent.disconnect(this.onTouchEndFn);
}
this.touchStartTime = 0;
this.onTouchStartFn = null;
this.onTouchEndFn = null;
};
var androidControls = new AndroidControls();
Script.scriptEnding.connect(function () {
androidControls.ending();
});
androidControls.init();
module.exports = androidControls;
}());

View file

@ -69,12 +69,12 @@ function touchEnd(event) {
var propertiesToGet = {};
propertiesToGet[overlayID] = ['url'];
var properties = Overlays.getOverlaysProperties(propertiesToGet);
if (properties[overlayID].url) {
if (properties[overlayID].url && !properties[overlayID].url.match(/\.qml$/)) {
Window.openUrl(properties[overlayID].url);
}
} else if (intersection && intersection.type == 'entity' && touchEntityID == intersection.obj.entityID) {
var properties = Entities.getEntityProperties(touchEntityID, ["sourceUrl"]);
if (properties.sourceUrl) {
if (properties.sourceUrl && !properties.sourceUrl.match(/\.qml$/)) {
Window.openUrl(properties.sourceUrl);
}
}

View file

@ -30,6 +30,7 @@ var radar = Script.require('./radar.js');
var uniqueColor = Script.require('./uniqueColor.js');
var displayNames = Script.require('./displayNames.js');
var clickWeb = Script.require('./clickWeb.js');
var androidControls = Script.require('./androidControls.js');
function printd(str) {
if (logEnabled) {
@ -99,10 +100,12 @@ function switchToMode(newMode) {
radar.startRadarMode();
displayNames.ending();
clickWeb.ending();
androidControls.ending();
} else if (currentMode == MODE_MY_VIEW) {
// nothing to do yet
displayNames.init();
clickWeb.init();
androidControls.init();
} else {
printd("Unknown view mode " + currentMode);
}
@ -121,4 +124,4 @@ Script.scriptEnding.connect(function () {
init();
}()); // END LOCAL_SCOPE
}()); // END LOCAL_SCOPE

View file

@ -1119,7 +1119,7 @@ function startRadar() {
function endRadar() {
printd("-- endRadar");
Camera.mode = "third person";
Camera.mode = "first person look at";
radar = false;
Controller.setVPadEnabled(true);