diff --git a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml new file mode 100644 index 0000000000..6e5bc50d32 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml @@ -0,0 +1,339 @@ +// +// TabletCustomQueryDialog.qml +// +// Created by Vlad Stelmahovsky on 3/27/17 +// Copyright 2017 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 QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" as ControlsUIT +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: root; + HifiConstants { id: hifi; } + + y: hifi.dimensions.tabletMenuHeader + + title: "" + visible: true; + property bool keyboardOverride: true + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none; + property string iconText: ""; + property int iconSize: 35; + onIconChanged: updateIcon(); + + property var textInput; + property var comboBox; + property var checkBox; + onTextInputChanged: { + if (textInput && textInput.text !== undefined) { + textField.text = textInput.text; + } + } + onComboBoxChanged: { + if (comboBox && comboBox.index !== undefined) { + comboBoxField.currentIndex = comboBox.index; + } + } + onCheckBoxChanged: { + if (checkBox && checkBox.checked !== undefined) { + checkBoxField.checked = checkBox.checked; + } + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + onKeyboardRaisedChanged: d.resize(); + + property var warning: ""; + property var result; + + property var implicitCheckState: null; + + property int titleWidth: 0; + onTitleWidthChanged: d.resize(); + + MouseArea { + width: parent.width + height: parent.height + } + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + function updateCheckbox() { + if (checkBox.disableForItems) { + var currentItemInDisableList = false; + for (var i in checkBox.disableForItems) { + if (comboBoxField.currentIndex === checkBox.disableForItems[i]) { + currentItemInDisableList = true; + break; + } + } + + if (currentItemInDisableList) { + checkBoxField.enabled = false; + if (checkBox.checkStateOnDisable !== null && checkBox.checkStateOnDisable !== undefined) { + root.implicitCheckState = checkBoxField.checked; + checkBoxField.checked = checkBox.checkStateOnDisable; + } + root.warning = checkBox.warningOnDisable; + } else { + checkBoxField.enabled = true; + if (root.implicitCheckState !== null) { + checkBoxField.checked = root.implicitCheckState; + root.implicitCheckState = null; + } + root.warning = ""; + } + } + } + + TabletModalFrame { + id: modalWindowItem + width: parent.width - 12 + height: 240 + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + + QtObject { + id: d; + readonly property int minWidth: 470 + readonly property int maxWidth: 470 + readonly property int minHeight: 240 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, 470); + var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + + (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + + (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); + + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? + d.maxHeight : targetHeight); + if (checkBoxField.visible && comboBoxField.visible) { + checkBoxField.width = extraInputs.width / 2; + comboBoxField.width = extraInputs.width / 2; + } else if (!checkBoxField.visible && comboBoxField.visible) { + comboBoxField.width = extraInputs.width; + } + } + } +// Item { +// anchors { +// // top: parent.top; +// // bottom: extraInputs.visible ? extraInputs.top : buttons.top; +// left: parent.left; +// right: parent.right; +// topMargin: 20 +// } + + // FIXME make a text field type that can be bound to a history for autocompletion + ControlsUIT.TextField { + id: textField; + label: root.textInput.label; + focus: root.textInput ? true : false; + visible: root.textInput ? true : false; + anchors { + top: parent.top + left: parent.left; + right: parent.right; + leftMargin: 5; rightMargin: 5; + topMargin: textField.controlHeight - textField.height + 5 + } + } + + ControlsUIT.Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + leftMargin: 5; rightMargin: 5; + top: textField.bottom + topMargin: raised ? hifi.dimensions.contentSpacing.y : 0 + } + } + // } + + Row { + id: extraInputs; + visible: Boolean(root.checkBox || root.comboBox); + anchors { + left: parent.left; + right: parent.right; + leftMargin: 5; rightMargin: 5; + top: keyboard.bottom; + topMargin: hifi.dimensions.contentSpacing.y + 20; + } + height: comboBoxField.controlHeight; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + + ControlsUIT.CheckBox { + id: checkBoxField; + text: root.checkBox.label; + focus: Boolean(root.checkBox); + visible: Boolean(root.checkBox); + // anchors { + // left: parent.left; + // bottom: parent.bottom; + // leftMargin: 6; // Magic number to align with warning icon + // bottomMargin: 6; + // } + } + + ControlsUIT.TabletComboBox { + id: comboBoxField; + label: root.comboBox.label; + focus: Boolean(root.comboBox); + visible: Boolean(root.comboBox); + // anchors { + // right: parent.right; + // bottom: parent.bottom; + // } + model: root.comboBox ? root.comboBox.items : []; + onAccepted: { + updateCheckbox(); + focus = true; + } + } + } + + Row { + id: buttons; + focus: true; + spacing: hifi.dimensions.contentSpacing.x; + layoutDirection: Qt.RightToLeft; + onHeightChanged: d.resize(); + onWidthChanged: { + d.resize(); + resizeWarningText(); + } + + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + bottomMargin: hifi.dimensions.contentSpacing.y; + leftMargin: 5; rightMargin: 5; + } + + function resizeWarningText() { + var rowWidth = buttons.width; + var buttonsWidth = acceptButton.width + cancelButton.width + hifi.dimensions.contentSpacing.x * 2; + var warningIconWidth = warningIcon.width + hifi.dimensions.contentSpacing.x; + warningText.width = rowWidth - buttonsWidth - warningIconWidth; + } + + ControlsUIT.Button { + id: cancelButton; + action: cancelAction; + } + + ControlsUIT.Button { + id: acceptButton; + action: acceptAction; + } + + Text { + id: warningText; + visible: Boolean(root.warning); + text: root.warning; + wrapMode: Text.WordWrap; + font.italic: true; + maximumLineCount: 2; + } + + HiFiGlyphs { + id: warningIcon; + visible: Boolean(root.warning); + text: hifi.glyphs.alert; + size: hifi.dimensions.controlLineHeight; + } + } +// }//column + Action { + id: cancelAction; + text: qsTr("Cancel"); + shortcut: Qt.Key_Escape; + onTriggered: { + root.result = null; + root.canceled(); + root.destroy(); + } + } + + Action { + id: acceptAction; + text: qsTr("Add"); + shortcut: Qt.Key_Return; + onTriggered: { + var result = {}; + if (textInput) { + result.textInput = textField.text; + } + if (comboBox) { + result.comboBox = comboBoxField.currentIndex; + result.comboBoxText = comboBoxField.currentText; + } + if (checkBox) { + result.checkBox = checkBoxField.enabled ? checkBoxField.checked : null; + } + root.result = JSON.stringify(result); + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return; + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger(); + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger(); + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + updateCheckbox(); + d.resize(); + textField.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 0aef9179e8..b55eca7493 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -151,11 +151,12 @@ TabletModalWindow { } } - /*TabletComboBox { + TabletComboBox { id: pathSelector - anchors { + z: 10 + anchors { top: parent.top - topMargin: hifi.dimensions.contentMargin.y + topMargin: (fileDialogItem.hasTitle ? (fileDialogItem.frameMarginTop + hifi.dimensions.modalDialogMargin.y) : hifi.dimension.modalDialogMargin.y) left: navControls.right leftMargin: hifi.dimensions.contentSpacing.x right: parent.right @@ -219,7 +220,7 @@ TabletModalWindow { fileTableView.forceActiveFocus(); } } - }*/ + } QtObject { id: d diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml new file mode 100644 index 0000000000..93e18c7aba --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -0,0 +1,614 @@ +// +// TabletAssetServer.qml +// +// Created by Vlad Stelmahovsky on 3/3/17 +// Copyright 2017 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 QtQuick.Dialogs 1.2 as OriginalDialogs +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "AssetServer" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + property var scripts: ScriptDiscoveryService; + property var assetProxyModel: Assets.proxyModel; + property var assetMappingsModel: Assets.mappingModel; + property var currentDirectory; + + Settings { + category: "Overlay.AssetServer" + property alias directory: root.currentDirectory + } + + Component.onCompleted: { + isHMD = HMD.active; + ApplicationInterface.uploadRequest.connect(uploadClicked); + assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); + reload(); + } + + function doDeleteFile(path) { + console.log("Deleting " + path); + + Assets.deleteMappings(path, function(err) { + if (err) { + console.log("Asset browser - error deleting path: ", path, err); + + box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished deleting path: ", path); + reload(); + } + }); + + } + + function doRenameFile(oldPath, newPath) { + + if (newPath[0] !== "/") { + newPath = "/" + newPath; + } + + if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') { + // this is a folder rename but the user neglected to add a trailing slash when providing a new path + newPath = newPath + "/"; + } + + if (Assets.isKnownFolder(newPath)) { + box = errorMessageBox("Cannot overwrite existing directory."); + box.selected.connect(reload); + } + + console.log("Asset browser - renaming " + oldPath + " to " + newPath); + + Assets.renameMapping(oldPath, newPath, function(err) { + if (err) { + console.log("Asset browser - error renaming: ", oldPath, "=>", newPath, " - error ", err); + box = errorMessageBox("There was an error renaming:\n" + oldPath + " to " + newPath + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished rename: ", oldPath, "=>", newPath); + } + + reload(); + }); + } + + function fileExists(path) { + return Assets.isKnownMapping(path); + } + + function askForOverwrite(path, callback) { + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.No, + title: "Overwrite File", + text: path + "\n" + "This file already exists. Do you want to overwrite it?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + callback(); + } + }); + } + + function canAddToWorld(path) { + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + + return supportedExtensions.reduce(function(total, current) { + return total | new RegExp(current).test(path); + }, false); + } + + function clear() { + Assets.mappingModel.clear(); + } + + function reload() { + Assets.mappingModel.refresh(); + treeView.selection.clear(); + } + + function handleGetMappingsError(errorString) { + errorMessageBox( + "There was a problem retreiving the list of assets from your Asset Server.\n" + + errorString + ); + } + + function addToWorld() { + var defaultURL = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + + if (!defaultURL || !canAddToWorld(defaultURL)) { + return; + } + + var SHAPE_TYPE_NONE = 0; + var SHAPE_TYPE_SIMPLE_HULL = 1; + var SHAPE_TYPE_SIMPLE_COMPOUND = 2; + var SHAPE_TYPE_STATIC_MESH = 3; + + var SHAPE_TYPES = []; + SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; + SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; + + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; + var DYNAMIC_DEFAULT = false; + var prompt = tabletRoot.customInputDialog({ + textInput: { + label: "Model URL", + text: defaultURL + }, + comboBox: { + label: "Automatic Collisions", + index: SHAPE_TYPE_DEFAULT, + items: SHAPE_TYPES + }, + checkBox: { + label: "Dynamic", + checked: DYNAMIC_DEFAULT, + disableForItems: [ + SHAPE_TYPE_STATIC_MESH + ], + checkStateOnDisable: false, + warningOnDisable: "Models with automatic collisions set to 'Exact' cannot be dynamic" + } + }); + + prompt.selected.connect(function (jsonResult) { + if (jsonResult) { + var result = JSON.parse(jsonResult); + var url = result.textInput.trim(); + var shapeType; + switch (result.comboBox) { + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + default: + shapeType = "none"; + } + + var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; + if (shapeType === "static-mesh" && dynamic) { + // The prompt should prevent this case + print("Error: model cannot be both static mesh and dynamic. This should never happen."); + } else if (url) { + var name = assetProxyModel.data(treeView.selection.currentIndex); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var gravity; + if (dynamic) { + // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a + // different scripting engine from QTScript. + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 10); + } else { + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0); + } + + print("Asset browser - adding asset " + url + " (" + name + ") to world."); + + // Entities.addEntity doesn't work from QML, so we use this. + Entities.addModelEntity(name, url, shapeType, dynamic, addPosition, gravity); + } + } + }); + } + + function copyURLToClipboard(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + if (!url) { + return; + } + Window.copyToClipboard(url); + } + + function renameEl(index, data) { + if (!index) { + return false; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return false; + } + + var destinationPath = path.split('/'); + destinationPath[destinationPath.length - (path[path.length - 1] === '/' ? 2 : 1)] = data; + destinationPath = destinationPath.join('/').trim(); + + if (path === destinationPath) { + return; + } + if (!fileExists(destinationPath)) { + doRenameFile(path, destinationPath); + } + } + function renameFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var object = tabletRoot.inputDialog({ + label: "Enter new path:", + current: path, + placeholderText: "Enter path here" + }); + object.selected.connect(function(destinationPath) { + destinationPath = destinationPath.trim(); + + if (path === destinationPath) { + return; + } + if (fileExists(destinationPath)) { + askForOverwrite(destinationPath, function() { + doRenameFile(path, destinationPath); + }); + } else { + doRenameFile(path, destinationPath); + } + }); + } + function deleteFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); + var typeString = isFolder ? 'folder' : 'file'; + + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes, + title: "Delete", + text: "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + doDeleteFile(path); + } + }); + } + + Timer { + id: doUploadTimer + property var url + property bool isConnected: false + interval: 5 + repeat: false + running: false + } + + property bool uploadOpen: false; + Timer { + id: timer + } + function uploadClicked(fileUrl) { + if (uploadOpen) { + return; + } + uploadOpen = true; + + function doUpload(url, dropping) { + var fileUrl = fileDialogHelper.urlToPath(url); + + var path = assetProxyModel.data(treeView.selection.currentIndex, 0x100); + var directory = path ? path.slice(0, path.lastIndexOf('/') + 1) : "/"; + var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1); + + Assets.uploadFile(fileUrl, directory + filename, + function() { + // Upload started + uploadSpinner.visible = true; + uploadButton.enabled = false; + uploadProgressLabel.text = "In progress..."; + }, + function(err, path) { + print(err, path); + if (err === "") { + uploadProgressLabel.text = "Upload Complete"; + timer.interval = 1000; + timer.repeat = false; + timer.triggered.connect(function() { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + }); + timer.start(); + console.log("Asset Browser - finished uploading: ", fileUrl); + reload(); + } else { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + + if (err !== -1) { + console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); + var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); + box.selected.connect(reload); + } + } + }, dropping); + } + + function initiateUpload(url) { + doUpload(doUploadTimer.url, false); + } + + if (fileUrl) { + doUpload(fileUrl, true); + } else { + var browser = tabletRoot.fileDialog({ + selectDirectory: false, + dir: currentDirectory + }); + + browser.canceled.connect(function() { + uploadOpen = false; + }); + + browser.selectedFile.connect(function(url) { + currentDirectory = browser.dir; + + // Initiate upload from a timer so that file browser dialog can close beforehand. + doUploadTimer.url = url; + if (!doUploadTimer.isConnected) { + doUploadTimer.triggered.connect(function() { initiateUpload(); }); + doUploadTimer.isConnected = true; + } + doUploadTimer.start(); + }); + } + } + + function errorMessageBox(message) { + return tabletRoot.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } + + Column { + width: parent.width + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 10 + + HifiControls.TabletContentSection { + id: assetDirectory + name: "Asset Directory" + isFirst: true + + HifiControls.VerticalSpacer {} + + Row { + id: buttonRow + width: parent.width + height: 30 + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.GlyphButton { + glyph: hifi.glyphs.reload + color: hifi.buttons.black + colorScheme: root.colorScheme + width: hifi.dimensions.controlLineHeight + + onClicked: root.reload() + } + + HifiControls.Button { + text: "Add To World" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 120 + + enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100)) + + onClicked: root.addToWorld() + } + + HifiControls.Button { + text: "Rename" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 80 + + onClicked: root.renameFile() + enabled: treeView.selection.hasSelection + } + + HifiControls.Button { + id: deleteButton + + text: "Delete" + color: hifi.buttons.red + colorScheme: root.colorScheme + width: 80 + + onClicked: root.deleteFile() + enabled: treeView.selection.hasSelection + } + } + + Menu { + id: contextMenu + title: "Edit" + property var url: "" + property var currentIndex: null + + MenuItem { + text: "Copy URL" + onTriggered: { + copyURLToClipboard(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Rename" + onTriggered: { + renameFile(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Delete" + onTriggered: { + deleteFile(contextMenu.currentIndex); + } + } + } + + } + HifiControls.Tree { + id: treeView + height: 430 + anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.left: parent.left + anchors.right: parent.right + + treeModel: assetProxyModel + canEdit: true + colorScheme: root.colorScheme + + modifyEl: renameEl + + MouseArea { + propagateComposedEvents: true + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } + } + } + } + + + HifiControls.TabletContentSection { + id: uploadSection + name: "Upload A File" + spacing: hifi.dimensions.contentSpacing.y + //anchors.bottom: parent.bottom + height: 65 + anchors.left: parent.left + anchors.right: parent.right + + Item { + height: parent.height + width: parent.width + HifiControls.Button { + id: uploadButton + anchors.right: parent.right + + text: "Choose File" + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + width: 155 + + onClicked: uploadClickedTimer.running = true + + // For some reason trigginer an API that enters + // an internal event loop directly from the button clicked + // trigger below causes the appliction to behave oddly. + // Most likely because the button onClicked handling is never + // completed until the function returns. + // FIXME find a better way of handling the input dialogs that + // doesn't trigger this. + Timer { + id: uploadClickedTimer + interval: 5 + repeat: false + running: false + onTriggered: uploadClicked(); + } + } + + Item { + id: uploadSpinner + visible: false + anchors.top: parent.top + anchors.left: parent.left + width: 40 + height: 32 + + Image { + id: image + width: 24 + height: 24 + source: "../../../images/Loading-Outer-Ring.png" + RotationAnimation on rotation { + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + Image { + width: 24 + height: 24 + source: "../../../images/Loading-Inner-H.png" + } + HifiControls.Label { + id: uploadProgressLabel + anchors.left: image.right + anchors.leftMargin: 10 + anchors.verticalCenter: image.verticalCenter + text: "In progress..." + colorScheme: root.colorScheme + } + } + } + } + } +} + diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 8eaddfb601..99c9351993 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -2,6 +2,7 @@ import QtQuick 2.0 import Hifi 1.0 import QtQuick.Controls 1.4 import "../../dialogs" + Item { id: tabletRoot objectName: "tabletRoot" @@ -29,7 +30,9 @@ Item { return openMessage; } + Component { id: customInputDialogBuilder; TabletCustomQueryDialog { } } function customInputDialog(properties) { + return customInputDialogBuilder.createObject(tabletRoot, properties); } Component { id: fileDialogBuilder; TabletFileDialog { } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 031e80283e..38534d4243 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -159,6 +159,7 @@ Item { readonly property vector2d menuPadding: Qt.vector2d(14, 102) readonly property real scrollbarBackgroundWidth: 18 readonly property real scrollbarHandleWidth: scrollbarBackgroundWidth - 2 + readonly property real tabletMenuHeader: 90 } Item { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a32acc474b..66d138103d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -499,6 +499,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -1620,14 +1621,7 @@ QString Application::getUserAgent() { return userAgent; } -uint64_t lastTabletUIToggle { 0 }; -const uint64_t toggleTabletUILockout { 500000 }; void Application::toggleTabletUI() const { - uint64_t now = usecTimestampNow(); - if (now - lastTabletUIToggle < toggleTabletUILockout) { - return; - } - lastTabletUIToggle = now; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); bool messageOpen = tablet->isMessageDialogOpen(); @@ -1972,7 +1966,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Quat", new Quat()); rootContext->setContextProperty("Vec3", new Vec3()); rootContext->setContextProperty("Uuid", new ScriptUUID()); - rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface()); + rootContext->setContextProperty("Assets", DependencyManager::get().data()); rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); rootContext->setContextProperty("Users", DependencyManager::get().data()); @@ -5839,7 +5833,21 @@ void Application::showAssetServerWidget(QString filePath) { emit uploadRequest(filePath); } }; - DependencyManager::get()->show(url, "AssetServer", startUpload); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + if (tablet->getToolbarMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + if (!tabletRoot && !isHMDMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("../../hifi/dialogs/TabletAssetServer.qml"); + tablet->pushOntoStack(url); + } + } + startUpload(nullptr, nullptr); } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index 459f01b512..b7fcea2491 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -20,6 +20,8 @@ #include #include +#include "DependencyManager.h" + class AssetMappingModel : public QStandardItemModel { Q_OBJECT @@ -39,10 +41,12 @@ private: QHash _pathToItemMap; }; -Q_DECLARE_METATYPE(AssetMappingModel*); +Q_DECLARE_METATYPE(AssetMappingModel*) -class AssetMappingsScriptingInterface : public QObject { +class AssetMappingsScriptingInterface : public QObject, public Dependency { Q_OBJECT + SINGLETON_DEPENDENCY + Q_PROPERTY(AssetMappingModel* mappingModel READ getAssetMappingModel CONSTANT) Q_PROPERTY(QAbstractProxyModel* proxyModel READ getProxyModel CONSTANT) public: diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index e1ea06c599..900c79fc3f 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -258,8 +258,3 @@ QVariant Line3DOverlay::getProperty(const QString& property) { Line3DOverlay* Line3DOverlay::createClone() const { return new Line3DOverlay(this); } - - -void Line3DOverlay::locationChanged(bool tellPhysics) { - // do nothing -} diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index aceecff6b2..c9ceac55a9 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -48,8 +48,6 @@ public: virtual Line3DOverlay* createClone() const override; - virtual void locationChanged(bool tellPhysics = true) override; - glm::vec3 getDirection() const { return _direction; } float getLength() const { return _length; } glm::vec3 getLocalStart() const { return getLocalPosition(); } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 454030cb1d..32fe26a697 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -37,12 +37,13 @@ #include #include "scripting/AccountScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" #include #include #include "FileDialogHelper.h" +#include "avatar/AvatarManager.h" #include "AudioClient.h" - static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; static const float METERS_TO_INCHES = 39.3701f; @@ -164,6 +165,10 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); @@ -176,16 +181,25 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); _webSurface->getRootContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Tablet", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Assets", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("pathToFonts", "../../"); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // Override min fps for tablet UI, for silky smooth scrolling - _webSurface->setMaxFps(90); + setMaxFPS(90); } } _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } +void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { + _desiredMaxFPS = maxFPS; + if (_webSurface) { + _webSurface->setMaxFps(_desiredMaxFPS); + _currentMaxFPS = _desiredMaxFPS; + } +} + void Web3DOverlay::render(RenderArgs* args) { if (!_visible || !getParentVisible()) { return; @@ -195,9 +209,11 @@ void Web3DOverlay::render(RenderArgs* args) { QSurface * currentSurface = currentContext->surface(); if (!_webSurface) { _webSurface = DependencyManager::get()->acquire(pickURL()); - _webSurface->setMaxFps(10); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } loadSourceURL(); _webSurface->resume(); _webSurface->resize(QSize(_resolution.x, _resolution.y)); @@ -247,6 +263,10 @@ void Web3DOverlay::render(RenderArgs* args) { _emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); _webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + } else { + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } } vec2 halfSize = getSize() / 2.0f; @@ -402,6 +422,11 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { _dpi = dpi.toFloat(); } + auto maxFPS = properties["maxFPS"]; + if (maxFPS.isValid()) { + _desiredMaxFPS = maxFPS.toInt(); + } + auto showKeyboardFocusHighlight = properties["showKeyboardFocusHighlight"]; if (showKeyboardFocusHighlight.isValid()) { _showKeyboardFocusHighlight = showKeyboardFocusHighlight.toBool(); @@ -421,6 +446,9 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "dpi") { return _dpi; } + if (property == "maxFPS") { + return _desiredMaxFPS; + } if (property == "showKeyboardFocusHighlight") { return _showKeyboardFocusHighlight; } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 2b9686919d..e71cac2452 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -31,6 +31,7 @@ public: QString pickURL(); void loadSourceURL(); + void setMaxFPS(uint8_t maxFPS); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -75,6 +76,9 @@ private: bool _pressed{ false }; QTouchDevice _touchDevice; + uint8_t _desiredMaxFPS { 10 }; + uint8_t _currentMaxFPS { 0 }; + QMetaObject::Connection _mousePressConnection; QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 06df75d451..3c33451f02 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2000,6 +2000,11 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { } } + auto currentBasis = getRecordingBasis(); + if (!currentBasis) { + currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); + } + if (json.contains(JSON_AVATAR_RELATIVE)) { // During playback you can either have the recording basis set to the avatar current state // meaning that all playback is relative to this avatars starting position, or @@ -2008,15 +2013,14 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { // The first is more useful for playing back recordings on your own avatar, while // the latter is more useful for playing back other avatars within your scene. - auto currentBasis = getRecordingBasis(); - if (!currentBasis) { - currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); - } - auto relativeTransform = Transform::fromJson(json[JSON_AVATAR_RELATIVE]); auto worldTransform = currentBasis->worldTransform(relativeTransform); setPosition(worldTransform.getTranslation()); setOrientation(worldTransform.getRotation()); + } else { + // We still set the position in the case that there is no movement. + setPosition(currentBasis->getTranslation()); + setOrientation(currentBasis->getRotation()); } if (json.contains(JSON_AVATAR_SCALE)) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 4dfa0b8f79..6f60f36556 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -310,24 +310,24 @@ function getFingerWorldLocation(hand) { // Object assign polyfill if (typeof Object.assign != 'function') { - Object.assign = function(target, varArgs) { - 'use strict'; - if (target == null) { - throw new TypeError('Cannot convert undefined or null to object'); - } - var to = Object(target); - for (var index = 1; index < arguments.length; index++) { - var nextSource = arguments[index]; - if (nextSource != null) { - for (var nextKey in nextSource) { - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } + Object.assign = function(target, varArgs) { + 'use strict'; + if (target == null) { + throw new TypeError('Cannot convert undefined or null to object'); } - } - } - return to; - }; + var to = Object(target); + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + if (nextSource != null) { + for (var nextKey in nextSource) { + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; } function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { @@ -1609,16 +1609,16 @@ function MyController(hand) { return true; }; this.entityIsCloneable = function(entityID) { - var entityProps = entityPropertiesCache.getGrabbableProps(entityID); - var props = entityPropertiesCache.getProps(entityID); - if (!props) { - return false; - } + var entityProps = entityPropertiesCache.getGrabbableProps(entityID); + var props = entityPropertiesCache.getProps(entityID); + if (!props) { + return false; + } - if (entityProps.hasOwnProperty("cloneable")) { - return entityProps.cloneable; - } - return false; + if (entityProps.hasOwnProperty("cloneable")) { + return entityProps.cloneable; + } + return false; } this.entityIsGrabbable = function(entityID) { var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); @@ -2330,7 +2330,7 @@ function MyController(hand) { // Can't set state of other controller to STATE_DISTANCE_HOLDING because then either: // (a) The entity would jump to line up with the formerly rotating controller's orientation, or // (b) The grab beam would need an orientation offset to the controller's true orientation. - // Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance + // Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance // rotating controller start distance holding the entity if it happens to be pointing at the entity. } return; @@ -2702,31 +2702,29 @@ function MyController(hand) { var userData = JSON.parse(grabbedProperties.userData); var grabInfo = userData.grabbableKey; if (grabInfo && grabInfo.cloneable) { - // Check if - var worldEntities = Entities.findEntitiesInBox(Vec3.subtract(MyAvatar.position, {x:25,y:25, z:25}), {x:50, y: 50, z: 50}) + var worldEntities = Entities.findEntities(MyAvatar.position, 50); var count = 0; worldEntities.forEach(function(item) { var item = Entities.getEntityProperties(item, ["name"]); - if (item.name === grabbedProperties.name) { + if (item.name.indexOf('-clone-' + grabbedProperties.id) !== -1) { count++; } }) + + var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0; + if (count >= limit && limit !== 0) { + delete limit; + return; + } + var cloneableProps = Entities.getEntityProperties(grabbedProperties.id); + cloneableProps.name = cloneableProps.name + '-clone-' + grabbedProperties.id; var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; - var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 10; var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; var cUserData = Object.assign({}, userData); var cProperties = Object.assign({}, cloneableProps); isClone = true; - if (count > limit) { - delete cloneableProps; - delete lifetime; - delete cUserData; - delete cProperties; - return; - } - delete cUserData.grabbableKey.cloneLifetime; delete cUserData.grabbableKey.cloneable; delete cUserData.grabbableKey.cloneDynamic; diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index c206a76e3f..c545e6bcee 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -45,6 +45,7 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var desktopOnlyViews = ['Mirror', 'Independent Mode', 'Entity Mode']; function onHmdChanged(isHmd) { + HMD.closeTablet(); if (isHmd) { button.editProperties({ icon: "icons/tablet-icons/switch-desk-i.svg", diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 71a868b292..4f774e5305 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -879,7 +879,7 @@ function loaded() { elCloneable.checked = false; elCloneableDynamic.checked = false; elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableLimit.value = 10; + elCloneableLimit.value = 0; elCloneableLifetime.value = 300; var parsedUserData = {} @@ -899,8 +899,6 @@ function loaded() { if ("cloneable" in parsedUserData["grabbableKey"]) { elCloneable.checked = parsedUserData["grabbableKey"].cloneable; elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableLimit.value = elCloneable.checked ? 10: 0; - elCloneableLifetime.value = elCloneable.checked ? 300: 0; elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic; elDynamic.checked = elCloneable.checked ? false: properties.dynamic; if (elCloneable.checked) { @@ -908,7 +906,7 @@ function loaded() { elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300; } if ("cloneLimit" in parsedUserData["grabbableKey"]) { - elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 10; + elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 0; } } } diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 8a0831a3cd..ed0b0e75f7 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -45,6 +45,10 @@ function calcSpawnInfo(hand, height) { var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; + if (!hand) { + hand = NO_HANDS; + } + if (HMD.active && hand !== NO_HANDS) { var handController = getControllerWorldLocation(hand, true); var controllerPosition = handController.position; @@ -96,7 +100,7 @@ function calcSpawnInfo(hand, height) { * @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand * @param clientOnly [bool] true indicates tablet model is only visible to client. */ -WebTablet = function (url, width, dpi, hand, clientOnly) { +WebTablet = function (url, width, dpi, hand, clientOnly, location) { var _this = this; @@ -134,6 +138,10 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(hand, true, tabletProperties); + if (location) { + tabletProperties.localPosition = location.localPosition; + tabletProperties.localRotation = location.localRotation; + } this.cleanUpOldTablets(); diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index d5065cb826..263c8822df 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -12,21 +12,47 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, MyAvatar */ +/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, + MyAvatar, Menu */ (function() { // BEGIN LOCAL_SCOPE var tabletShown = false; - var tabletLocation = null; + var tabletRezzed = false; var activeHand = null; + var DEFAULT_WIDTH = 0.4375; + var DEFAULT_TABLET_SCALE = 100; + var preMakeTime = Date.now(); + var validCheckTime = Date.now(); + var debugTablet = false; + UIWebTablet = null; Script.include("../libraries/WebTablet.js"); - function showTabletUI() { - tabletShown = true; - print("show tablet-ui"); + function tabletIsValid() { + if (!UIWebTablet) { + return false; + } + if (UIWebTablet.tabletIsOverlay && Overlays.getProperty(HMD.tabletID, "type") != "model") { + if (debugTablet) { + print("TABLET is invalid due to frame: " + JSON.stringify(Overlays.getProperty(HMD.tabletID, "type"))); + } + return false; + } + if (Overlays.getProperty(HMD.homeButtonID, "type") != "sphere" || + Overlays.getProperty(HMD.tabletScreenID, "type") != "web3d") { + if (debugTablet) { + print("TABLET is invalid due to other"); + } + return false; + } + return true; + } - var DEFAULT_WIDTH = 0.4375; - var DEFAULT_TABLET_SCALE = 100; + + function rezTablet() { + if (debugTablet) { + print("TABLET rezzing"); + } var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; var TABLET_SCALE = DEFAULT_TABLET_SCALE; if (toolbarMode) { @@ -34,37 +60,98 @@ } else { TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; } - UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (TABLET_SCALE / 100), null, activeHand, true); + + UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", + DEFAULT_WIDTH * (TABLET_SCALE / 100), + null, activeHand, true); UIWebTablet.register(); HMD.tabletID = UIWebTablet.tabletEntityID; HMD.homeButtonID = UIWebTablet.homeButtonID; HMD.tabletScreenID = UIWebTablet.webOverlayID; + + tabletRezzed = true; + } + + function showTabletUI() { + tabletShown = true; + + if (!tabletRezzed) { + rezTablet(false); + } + + if (UIWebTablet && tabletRezzed) { + if (debugTablet) { + print("TABLET in showTabletUI, already rezzed"); + } + var tabletProperties = {}; + UIWebTablet.calculateTabletAttachmentProperties(activeHand, true, tabletProperties); + tabletProperties.visible = true; + if (UIWebTablet.tabletIsOverlay) { + Overlays.editOverlay(HMD.tabletID, tabletProperties); + } + Overlays.editOverlay(HMD.homeButtonID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 }); + } } function hideTabletUI() { tabletShown = false; - print("hide tablet-ui"); + if (!UIWebTablet) { + return; + } + + if (UIWebTablet.tabletIsOverlay) { + if (debugTablet) { + print("TABLET hide"); + } + if (Settings.getValue("tabletVisibleToOthers")) { + closeTabletUI(); + } else { + // Overlays.editOverlay(HMD.tabletID, { localPosition: { x: -1000, y: 0, z:0 } }); + Overlays.editOverlay(HMD.tabletID, { visible: false }); + Overlays.editOverlay(HMD.homeButtonID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 1 }); + } + } else { + closeTabletUI(); + } + } + + function closeTabletUI() { + tabletShown = false; if (UIWebTablet) { if (UIWebTablet.onClose) { UIWebTablet.onClose(); } - tabletLocation = UIWebTablet.getLocation(); + if (debugTablet) { + print("TABLET close"); + } UIWebTablet.unregister(); UIWebTablet.destroy(); UIWebTablet = null; HMD.tabletID = null; HMD.homeButtonID = null; HMD.tabletScreenID = null; + } else if (debugTablet) { + print("TABLET closeTabletUI, UIWebTablet is null"); } + tabletRezzed = false; } + function updateShowTablet() { + var MSECS_PER_SEC = 1000.0; + var now = Date.now(); // close the WebTablet if it we go into toolbar mode. var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var visibleToOthers = Settings.getValue("tabletVisibleToOthers"); + if (tabletShown && toolbarMode) { - hideTabletUI(); + closeTabletUI(); HMD.closeTablet(); return; } @@ -78,19 +165,48 @@ tablet.updateAudioBar(currentMicLevel); } - if (tabletShown && UIWebTablet && Overlays.getOverlayType(UIWebTablet.webOverlayID) != "web3d") { - // when we switch domains, the tablet entity gets destroyed and recreated. this causes - // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any - // other reason, close the tablet. - hideTabletUI(); - HMD.closeTablet(); - } else if (HMD.showTablet && !tabletShown && !toolbarMode) { - UserActivityLogger.openedTablet(Settings.getValue("tabletVisibleToOthers")); + if (validCheckTime - now > MSECS_PER_SEC) { + validCheckTime = now; + if (tabletRezzed && UIWebTablet && !tabletIsValid()) { + // when we switch domains, the tablet entity gets destroyed and recreated. this causes + // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any + // other reason, close the tablet. + closeTabletUI(); + HMD.closeTablet(); + if (debugTablet) { + print("TABLET autodestroying"); + } + } + } + + + if (HMD.showTablet && !tabletShown && !toolbarMode) { + UserActivityLogger.openedTablet(visibleToOthers); showTabletUI(); } else if (!HMD.showTablet && tabletShown) { UserActivityLogger.closedTablet(); - hideTabletUI(); + if (visibleToOthers) { + closeTabletUI(); + } else { + hideTabletUI(); + } } + + // if the tablet is an overlay, attempt to pre-create it and then hide it so that when it's + // summoned, it will appear quickly. + if (!toolbarMode && !visibleToOthers) { + if (now - preMakeTime > MSECS_PER_SEC) { + preMakeTime = now; + if (!tabletIsValid()) { + closeTabletUI(); + rezTablet(false); + tabletShown = false; + } else if (!tabletShown) { + hideTabletUI(); + } + } + } + } function toggleHand(channel, hand, senderUUID, localOnly) { @@ -131,7 +247,9 @@ } Script.scriptEnding.connect(function () { - Entities.deleteEntity(HMD.tabletID); + var tabletID = HMD.tabletID; + Entities.deleteEntity(tabletID); + Overlays.deleteOverlay(tabletID) HMD.tabletID = null; HMD.homeButtonID = null; HMD.tabletScreenID = null;