diff --git a/.eslintrc.js b/.eslintrc.js index 6183fa8aec..c708decc51 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,6 +40,7 @@ module.exports = { "Settings": false, "SoundCache": false, "Stats": false, + "Tablet": false, "TextureCache": false, "Toolbars": false, "Uuid": false, @@ -61,7 +62,7 @@ module.exports = { "eqeqeq": ["error", "always"], "indent": ["error", 4, { "SwitchCase": 1 }], "keyword-spacing": ["error", { "before": true, "after": true }], - "max-len": ["error", 128, 4], + "max-len": ["error", 192, 4], "new-cap": ["error"], "no-floating-decimal": ["error"], //"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }], diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 87eb5ee1ab..d5e06504a6 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -61,7 +61,7 @@ void AudioMixerClientData::processPackets() { _packetQueue.node.clear(); while (!_packetQueue.empty()) { - auto& packet = _packetQueue.back(); + auto& packet = _packetQueue.front(); switch (packet->getType()) { case PacketType::MicrophoneAudioNoEcho: @@ -548,9 +548,6 @@ AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsi _zone = box; unsigned int oldFrame = _frame.exchange(frame, std::memory_order_release); Q_UNUSED(oldFrame); - - // check the precondition - assert(oldFrame == 0 || frame == (oldFrame + 1)); } } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 1047e10570..8d76cda2f1 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -44,7 +44,7 @@ public: AvatarAudioStream* getAvatarAudioStream(); // returns whether self (this data's node) should ignore node, memoized by frame - // precondition: frame is monotonically increasing after first call + // precondition: frame is increasing after first call (including overflow wrap) bool shouldIgnore(SharedNodePointer self, SharedNodePointer node, unsigned int frame); // the following methods should be called from the AudioMixer assignment thread ONLY @@ -131,7 +131,7 @@ private: // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) // preconditions: - // - frame is monotonically increasing after first call + // - frame is increasing after first call (including overflow wrap) // - there are no references left from calls to getIgnoreZone(frame - 1) IgnoreZone& get(unsigned int frame); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index bf85918145..45b04c4189 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -410,7 +410,7 @@ void AvatarMixer::broadcastAvatarData() { bool isInView = nodeData->otherAvatarInView(otherNodeBox); // this throttles the extra data to only be sent every Nth message - if (!isInView && getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) { + if (!isInView && !getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) { return; } @@ -572,6 +572,7 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointerreadPrimitive(&isRequesting); nodeData->setRequestsDomainListData(isRequesting); + qDebug() << "node" << nodeData->getNodeID() << "requestsDomainListData" << isRequesting; } } } diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 02dc552dae..425bea2c38 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "EntityServer.h" #include "EntityServerConsts.h" @@ -71,6 +72,7 @@ OctreePointer EntityServer::createTree() { DependencyManager::registerInheritance(); DependencyManager::set(tree); + DependencyManager::set(std::static_pointer_cast(tree)); return tree; } @@ -292,96 +294,26 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio } else { tree->setEntityScriptSourceWhitelist(""); } - - if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) { - // Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up. - std::static_pointer_cast(_tree)->setHasEntityFilter(true); - // Now fetch script from file asynchronously. - QUrl scriptURL(_entityEditFilter); - - // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) - if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { - qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; - scriptRequestFinished(); - return; - } - auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL); - if (!scriptRequest) { - qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString(); - scriptRequestFinished(); - return; - } - // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own. - connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished); - // FIXME: handle atp rquests setup here. See Agent::requestScript() - qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); - scriptRequest->send(); - qDebug() << "script request sent"; + + auto entityEditFilters = DependencyManager::get(); + + QString filterURL; + if (readOptionString("entityEditFilter", settingsSectionObject, filterURL) && !filterURL.isEmpty()) { + // connect the filterAdded signal, and block edits until you hear back + connect(entityEditFilters.data(), &EntityEditFilters::filterAdded, this, &EntityServer::entityFilterAdded); + + entityEditFilters->addFilter(EntityItemID(), filterURL); } } -// Copied from ScriptEngine.cpp. We should make this a class method for reuse. -// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point. -static bool hasCorrectSyntax(const QScriptProgram& program) { - const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); - if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { - const auto error = syntaxCheck.errorMessage(); - const auto line = QString::number(syntaxCheck.errorLineNumber()); - const auto column = QString::number(syntaxCheck.errorColumnNumber()); - const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); - qCritical() << qPrintable(message); - return false; - } - return true; -} -static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { - if (engine.hasUncaughtException()) { - const auto backtrace = engine.uncaughtExceptionBacktrace(); - const auto exception = engine.uncaughtException().toString(); - const auto line = QString::number(engine.uncaughtExceptionLineNumber()); - engine.clearExceptions(); - - static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; - auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); - if (!backtrace.empty()) { - static const auto lineSeparator = "\n "; - message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); +void EntityServer::entityFilterAdded(EntityItemID id, bool success) { + if (id.isInvalidID()) { + if (success) { + qDebug() << "entity edit filter for " << id << "added successfully"; + } else { + qDebug() << "entity edit filter unsuccessfully added, all edits will be rejected for those without lock rights."; } - qCritical() << qPrintable(message); - return true; } - return false; -} -void EntityServer::scriptRequestFinished() { - qDebug() << "script request completed"; - auto scriptRequest = qobject_cast(sender()); - const QString urlString = scriptRequest->getUrl().toString(); - if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { - auto scriptContents = scriptRequest->getData(); - qInfo() << "Downloaded script:" << scriptContents; - QScriptProgram program(scriptContents, urlString); - if (hasCorrectSyntax(program)) { - _entityEditFilterEngine.evaluate(scriptContents); - if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) { - std::static_pointer_cast(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() { - return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter); - }); - scriptRequest->deleteLater(); - qDebug() << "script request filter processed"; - return; - } - } - } else if (scriptRequest) { - qCritical() << "Failed to download script at" << urlString; - // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. - qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); - } else { - qCritical() << "Failed to create script request."; - } - // Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't. - // Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities. - qDebug() << "script request failure causing stop"; - stop(); } void EntityServer::nodeAdded(SharedNodePointer node) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index f142145d5f..325435fe7e 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -63,13 +63,13 @@ public slots: virtual void nodeAdded(SharedNodePointer node) override; virtual void nodeKilled(SharedNodePointer node) override; void pruneDeletedEntities(); + void entityFilterAdded(EntityItemID id, bool success); protected: virtual OctreePointer createTree() override; private slots: void handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode); - void scriptRequestFinished(); private: SimpleEntitySimulationPointer _entitySimulation; @@ -77,9 +77,6 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; - - QString _entityEditFilter{}; - QScriptEngine _entityEditFilterEngine{}; }; #endif // hifi_EntityServer_h diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 746e599d4e..2ce537a5a0 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -20,7 +20,7 @@ endif () symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources") # link the shared hifi libraries -link_hifi_libraries(embedded-webserver networking shared) +link_hifi_libraries(embedded-webserver networking shared avatars) # find OpenSSL find_package(OpenSSL REQUIRED) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 7e49dc55a4..661a6213b8 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -29,7 +29,8 @@ #include #include #include - +#include //for KillAvatarReason +#include #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -439,7 +440,7 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key foreach (QVariant permsHash, permissionsList) { NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); - + NodePermissionsKey idKey = perms->getKey(); if (mapPointer->contains(idKey)) { @@ -484,7 +485,7 @@ void DomainServerSettingsManager::unpackPermissions() { // make sure that this permission row is for a non-empty hardware if (perms->getKey().first.isEmpty()) { _macPermissions.remove(perms->getKey()); - + // we removed a row from the MAC permissions, we'll need a re-pack needPack = true; } @@ -555,7 +556,7 @@ void DomainServerSettingsManager::unpackPermissions() { QList> permissionsSets; permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() << _groupPermissions.get() << _groupForbiddens.get() - << _ipPermissions.get() << _macPermissions.get() + << _ipPermissions.get() << _macPermissions.get() << _machineFingerprintPermissions.get(); foreach (auto permissionSet, permissionsSets) { @@ -668,77 +669,84 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointerclear(NodePermissions::Permission::canConnectToDomain); } else { - // otherwise we apply the kick to the IP from active socket for this node and the MAC address - - // remove connect permissions for the IP (falling back to the public socket if not yet active) - auto& kickAddress = matchingNode->getActiveSocket() - ? matchingNode->getActiveSocket()->getAddress() - : matchingNode->getPublicSocket().getAddress(); - - // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server - // it is always 127.0.0.1, but looking at the public and local addresses just to be sure - // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we - // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. - if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || - kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || - kickAddress.isLoopback() ) { - qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; - return; - } - NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); - - // check if there were already permissions for the IP - bool hadIPPermissions = hasPermissionsForIP(kickAddress); - - // grab or create permissions for the given IP address - auto ipPermissions = _ipPermissions[ipAddressKey]; - - if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - newPermissions = true; - - ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); - } - - // potentially remove connect permissions for the MAC address and machine fingerprint + // remove connect permissions for the machine fingerprint DomainServerNodeData* nodeData = static_cast(matchingNode->getLinkedData()); if (nodeData) { - // mac address first - NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0); + // get this machine's fingerprint + auto domainServerFingerprint = FingerprintUtils::getMachineFingerprint(); - bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress()); - - auto macPermissions = _macPermissions[macAddressKey]; - - if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - newPermissions = true; - - macPermissions->clear(NodePermissions::Permission::canConnectToDomain); + if (nodeData->getMachineFingerprint() == domainServerFingerprint) { + qWarning() << "attempt to kick node running on same machine as domain server (by fingerprint), ignoring KickRequest"; + return; } - - // now for machine fingerprint NodePermissionsKey machineFingerprintKey(nodeData->getMachineFingerprint().toString(), 0); - + + // check if there were already permissions for the fingerprint bool hadFingerprintPermissions = hasPermissionsForMachineFingerprint(nodeData->getMachineFingerprint()); - + + // grab or create permissions for the given fingerprint auto fingerprintPermissions = _machineFingerprintPermissions[machineFingerprintKey]; - + + // write them if (!hadFingerprintPermissions || fingerprintPermissions->can(NodePermissions::Permission::canConnectToDomain)) { newPermissions = true; fingerprintPermissions->clear(NodePermissions::Permission::canConnectToDomain); } + } else { + // if no node data, all we can do is IP address + auto& kickAddress = matchingNode->getActiveSocket() + ? matchingNode->getActiveSocket()->getAddress() + : matchingNode->getPublicSocket().getAddress(); + + // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server + // it is always 127.0.0.1, but looking at the public and local addresses just to be sure + // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we + // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. + if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || + kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || + kickAddress.isLoopback() ) { + qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; + return; + } + + + NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); + + // check if there were already permissions for the IP + bool hadIPPermissions = hasPermissionsForIP(kickAddress); + + // grab or create permissions for the given IP address + auto ipPermissions = _ipPermissions[ipAddressKey]; + + if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } } } + // if we are here, then we kicked them, so send the KillAvatar message + auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true); + packet->write(nodeUUID.toRfc4122()); + packet->writePrimitive(KillAvatarReason::NoReason); + + // send to avatar mixer, it sends the kill to everyone else + limitedNodeList->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); + if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) << "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()); // we've changed permissions, time to store them to disk and emit our signal to say they have changed packPermissions(); - } else { - emit updateNodePermissions(); } + // we emit this no matter what -- though if this isn't a new permission probably 2 people are racing to kick and this + // person lost the race. No matter, just be sure this is called as otherwise it takes like 10s for the person being banned + // to go away + emit updateNodePermissions(); + } else { qWarning() << "Node kick request received for unknown node. Refusing to process."; } diff --git a/interface/resources/icons/tablet-icons/blank.svg b/interface/resources/icons/tablet-icons/blank.svg new file mode 100644 index 0000000000..ae463c4242 --- /dev/null +++ b/interface/resources/icons/tablet-icons/blank.svg @@ -0,0 +1,48 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/empty-toolbar-button.svg b/interface/resources/icons/tablet-icons/empty-toolbar-button.svg new file mode 100644 index 0000000000..19791e6c29 --- /dev/null +++ b/interface/resources/icons/tablet-icons/empty-toolbar-button.svg @@ -0,0 +1,81 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 4c81027211..3e6e5b6764 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -48,7 +48,16 @@ OriginalDesktop.Desktop { // This used to create sysToolbar dynamically with a call to getToolbar() within onCompleted. // Beginning with QT 5.6, this stopped working, as anything added to toolbars too early got // wiped during startup. - + Toolbar { + id: sysToolbar; + objectName: "com.highfidelity.interface.toolbar.system"; + anchors.horizontalCenter: settings.constrainToolbarToCenterX ? desktop.horizontalCenter : undefined; + // Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained. + x: sysToolbar.x + y: 50 + shown: false + } + Settings { id: settings; category: "toolbar"; @@ -58,8 +67,9 @@ OriginalDesktop.Desktop { settings.constrainToolbarToCenterX = constrain; } property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar - return map; })({}); - + map[sysToolbar.objectName] = sysToolbar; + return map; + })({}); Component.onCompleted: { WebEngine.settings.javascriptCanOpenWindows = true; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 0c7104fba5..c1fea7c09b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -360,7 +360,7 @@ Rectangle { TextMetrics { id: displayNameHeaderMetrics text: displayNameHeader.title - font: displayNameHeader.font + // font: displayNameHeader.font // was this always undefined? giving error now... } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { @@ -426,7 +426,6 @@ Rectangle { onExited: adminHelpText.color = hifi.colors.redHighlight } } - } HifiControls.Keyboard { id: keyboard @@ -438,6 +437,7 @@ Rectangle { right: parent.right } } + } // Timer used when selecting table rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index c154ac0f49..e0deab64b6 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -97,10 +97,12 @@ FocusScope { menuPopperUpper.closeLastMenu(); } - function setRootMenu(menu) { - tabletMenu.rootMenu = menu + function setRootMenu(rootMenu, subMenu) { + tabletMenu.subMenu = subMenu; + tabletMenu.rootMenu = rootMenu; buildMenu() } + function buildMenu() { // Build submenu if specified. if (subMenu !== "") { diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index 1845396230..92e7f59524 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -83,7 +83,7 @@ FocusScope { } function recalcSize() { - if (model.count !== count || !visible) { + if (!model || model.count !== count || !visible) { return; } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 481c7846a9..1fb31e5619 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -6,7 +6,9 @@ Item { objectName: "tabletRoot" property string username: "Unknown user" property var eventBridge; - property string option: "" + + property var rootMenu; + property string subMenu: "" signal showDesktop(); @@ -14,7 +16,13 @@ Item { option = value; } + function setMenuProperties(rootMenu, subMenu) { + tabletRoot.rootMenu = rootMenu; + tabletRoot.subMenu = subMenu; + } + function loadSource(url) { + loader.source = ""; // make sure we load the qml fresh each time. loader.source = url; } @@ -77,13 +85,15 @@ Item { if (loader.item.hasOwnProperty("sendToScript")) { loader.item.sendToScript.connect(tabletRoot.sendToScript); } - if (loader.item.hasOwnProperty("subMenu")) { - loader.item.subMenu = option; + if (loader.item.hasOwnProperty("setRootMenu")) { + loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); } loader.item.forceActiveFocus(); } } width: 480 - height: 720 + height: 706 + + function setShown(value) {} } diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml new file mode 100644 index 0000000000..5f842df7b7 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -0,0 +1,111 @@ +// +// WindowRoot.qml +// +// Created by Anthony Thibault on 14 Feb 2017 +// 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 +// +// This qml is used when tablet content is shown on the 2d overlay ui +// TODO: FIXME: this is practically identical to TabletRoot.qml + +import "../../windows" as Windows +import QtQuick 2.0 +import Hifi 1.0 + +Windows.ScrollingWindow { + id: tabletRoot + objectName: "tabletRoot" + property string username: "Unknown user" + property var eventBridge; + + property var rootMenu; + property string subMenu: "" + + shown: false + resizable: false + + signal showDesktop(); + + function setMenuProperties(rootMenu, subMenu) { + tabletRoot.rootMenu = rootMenu; + tabletRoot.subMenu = subMenu; + } + + function loadSource(url) { + loader.source = ""; // make sure we load the qml fresh each time. + loader.source = url; + } + + function loadWebUrl(url, injectedJavaScriptUrl) { + loader.item.url = url; + loader.item.scriptURL = injectedJavaScriptUrl; + } + + // used to send a message from qml to interface script. + signal sendToScript(var message); + + // used to receive messages from interface script + function fromScript(message) { + if (loader.item.hasOwnProperty("fromScript")) { + loader.item.fromScript(message); + } + } + + SoundEffect { + id: buttonClickSound + volume: 0.1 + source: "../../../sounds/Gamemaster-Audio-button-click.wav" + } + + function playButtonClickSound() { + // Because of the asynchronous nature of initalization, it is possible for this function to be + // called before the C++ has set the globalPosition context variable. + if (typeof globalPosition !== 'undefined') { + buttonClickSound.play(globalPosition); + } + } + + function toggleMicEnabled() { + ApplicationInterface.toggleMuteAudio(); + } + + function setUsername(newUsername) { + username = newUsername; + } + + Loader { + id: loader + objectName: "loader" + asynchronous: false + + height: pane.scrollHeight + width: pane.contentWidth + anchors.left: parent.left + anchors.top: parent.top + + onLoaded: { + if (loader.item.hasOwnProperty("eventBridge")) { + loader.item.eventBridge = eventBridge; + + // Hook up callback for clara.io download from the marketplace. + eventBridge.webEventReceived.connect(function (event) { + if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + } + }); + } + if (loader.item.hasOwnProperty("sendToScript")) { + loader.item.sendToScript.connect(tabletRoot.sendToScript); + } + if (loader.item.hasOwnProperty("setRootMenu")) { + loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); + } + loader.item.forceActiveFocus(); + } + } + + implicitWidth: 480 + implicitHeight: 706 +} diff --git a/interface/resources/qml/hifi/toolbars/StateImage.qml b/interface/resources/qml/hifi/toolbars/StateImage.qml index ee0778626d..e0389c5e02 100644 --- a/interface/resources/qml/hifi/toolbars/StateImage.qml +++ b/interface/resources/qml/hifi/toolbars/StateImage.qml @@ -29,6 +29,7 @@ Item { id: image y: -parent.yOffset; width: parent.width + source: "../../../icons/tablet-icons/empty-toolbar-button.svg" } } diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 01ce74cf6e..0080e49815 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -25,7 +25,7 @@ Window { property real buttonSize: 50; property var buttons: [] property var container: horizontal ? row : column - + Settings { category: "toolbar/" + window.objectName property alias x: window.x @@ -49,6 +49,7 @@ Window { id: content implicitHeight: horizontal ? row.height : column.height implicitWidth: horizontal ? row.width : column.width + property bool wasVisibleBeforeBeingPinned: false Row { id: row @@ -65,19 +66,11 @@ Window { Connections { target: desktop onPinnedChanged: { - if (!window.pinned) { - return; - } - var newPinned = desktop.pinned; - for (var i in buttons) { - var child = buttons[i]; - if (desktop.pinned) { - if (!child.pinned) { - child.visible = false; - } - } else { - child.visible = true; - } + if (desktop.pinned) { + content.wasVisibleBeforeBeingPinned = window.visible; + window.visible = false; + } else { + window.visible = content.wasVisibleBeforeBeingPinned; } } } @@ -106,6 +99,24 @@ Window { return buttons[index]; } + function sortButtons() { + var children = []; + for (var i = 0; i < container.children.length; i++) { + children[i] = container.children[i]; + } + + children.sort(function (a, b) { + if (a.sortOrder === b.sortOrder) { + // subsort by stableOrder, because JS sort is not stable in qml. + return a.stableOrder - b.stableOrder; + } else { + return a.sortOrder - b.sortOrder; + } + }); + + container.children = children; + } + function addButton(properties) { properties = properties || {} @@ -123,8 +134,12 @@ Window { properties.opacity = 0; result = toolbarButtonBuilder.createObject(container, properties); buttons.push(result); + result.opacity = 1; updatePinned(); + + sortButtons(); + return result; } @@ -137,6 +152,10 @@ Window { buttons[index].destroy(); buttons.splice(index, 1); updatePinned(); + + if (buttons.length === 0) { + visible = false; + } } function updatePinned() { diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index 91c992bf0d..cab5b14d5c 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -11,12 +11,33 @@ StateImage { property int imageOnOut: 0 property int imageOnIn: 2 + property string text: "" + property string hoverText: button.text + property string activeText: button.text + property string activeHoverText: button.activeText + + property string icon: "icons/tablet-icons/blank.svg" + property string hoverIcon: button.icon + property string activeIcon: button.icon + property string activeHoverIcon: button.activeIcon + + property int sortOrder: 100 + property int stableSortOrder: 0 + signal clicked() function changeProperty(key, value) { button[key] = value; } + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + function updateState() { if (!button.isEntered && !button.isActive) { buttonState = imageOffOut; @@ -38,7 +59,7 @@ StateImage { running: false onTriggered: button.clicked(); } - + MouseArea { id: mouseArea hoverEnabled: true @@ -53,5 +74,28 @@ StateImage { updateState(); } } + + Image { + id: icon + width: 28 + height: 28 + anchors.bottom: caption.top + anchors.bottomMargin: 0 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.Stretch + source: urlHelper(button.isActive ? (button.isEntered ? button.activeHoverIcon : button.activeIcon) : (button.isEntered ? button.hoverIcon : button.icon)) + } + + Text { + id: caption + color: button.isActive ? "#000000" : "#ffffff" + text: button.isActive ? (button.isEntered ? button.activeHoverText : button.activeText) : (button.isEntered ? button.hoverText : button.text) + font.bold: false + font.pixelSize: 9 + anchors.bottom: parent.bottom + anchors.bottomMargin: 5 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } } diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index d22d8ecbe8..20216ed7ae 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -85,6 +85,10 @@ Fadable { function setDefaultFocus() {} // Default function; can be overridden by dialogs. + function setShown(value) { + window.shown = value; + } + property var rectifier: Timer { property bool executing: false; interval: 100 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 488e97b5e6..3a246fa166 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -545,6 +545,8 @@ Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; +const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; +const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : QApplication(argc, argv), @@ -565,6 +567,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), + _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), + _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _scaleMirror(1.0f), _rotateMirror(0.0f), @@ -831,6 +835,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); // Save avatar location immediately after a teleport. connect(myAvatar.get(), &MyAvatar::positionGoneTo, @@ -1537,6 +1542,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); connect(&domainHandler, &DomainHandler::hostnameChanged, this, &Application::addAssetToWorldMessageClose); + + updateSystemTabletMode(); } void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { @@ -2330,6 +2337,16 @@ void Application::setDesktopTabletScale(float desktopTabletScale) { _desktopTabletScale.set(desktopTabletScale); } +void Application::setDesktopTabletBecomesToolbarSetting(bool value) { + _desktopTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setHmdTabletBecomesToolbarSetting(bool value) { + _hmdTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + void Application::setSettingConstrainToolbarPosition(bool setting) { _constrainToolbarPosition.set(setting); DependencyManager::get()->setConstrainToolbarToCenterX(setting); @@ -5462,6 +5479,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); + DependencyManager::get().data()->setToolbarScriptingInterface(DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, CustomPromptResultToScriptValue, CustomPromptResultFromScriptValue); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, @@ -6854,6 +6873,14 @@ void Application::updateThreadPoolCount() const { QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); } +void Application::updateSystemTabletMode() { + if (isHMDMode()) { + DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); + } else { + DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); + } +} + void Application::toggleMuteAudio() { auto menu = Menu::getInstance(); menu->setIsOptionChecked(MenuOption::MuteAudio, !menu->isOptionChecked(MenuOption::MuteAudio)); diff --git a/interface/src/Application.h b/interface/src/Application.h index cab830ec88..5fc79bedb5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -214,6 +214,11 @@ public: float getDesktopTabletScale() { return _desktopTabletScale.get(); } void setDesktopTabletScale(float desktopTabletScale); + bool getDesktopTabletBecomesToolbarSetting() { return _desktopTabletBecomesToolbarSetting.get(); } + void setDesktopTabletBecomesToolbarSetting(bool value); + bool getHmdTabletBecomesToolbarSetting() { return _hmdTabletBecomesToolbarSetting.get(); } + void setHmdTabletBecomesToolbarSetting(bool value); + float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } void setSettingConstrainToolbarPosition(bool setting); @@ -310,6 +315,7 @@ public slots: bool exportEntities(const QString& filename, float x, float y, float z, float scale); bool importEntities(const QString& url); void updateThreadPoolCount() const; + void updateSystemTabletMode(); static void setLowVelocityFilter(bool lowVelocityFilter); Q_INVOKABLE void loadDialog(); @@ -550,6 +556,8 @@ private: Setting::Handle _fieldOfView; Setting::Handle _hmdTabletScale; Setting::Handle _desktopTabletScale; + Setting::Handle _desktopTabletBecomesToolbarSetting; + Setting::Handle _hmdTabletBecomesToolbarSetting; Setting::Handle _constrainToolbarPosition; float _scaleMirror; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 6377cda281..dd05d5c0e1 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -92,6 +92,16 @@ void setupPreferences() { preference->setMax(500); preferences->addPreference(preference); } + { + auto getter = []()->bool { return qApp->getDesktopTabletBecomesToolbarSetting(); }; + auto setter = [](bool value) { qApp->setDesktopTabletBecomesToolbarSetting(value); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "Desktop Tablet Becomes Toolbar", getter, setter)); + } + { + auto getter = []()->bool { return qApp->getHmdTabletBecomesToolbarSetting(); }; + auto setter = [](bool value) { qApp->setHmdTabletBecomesToolbarSetting(value); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter)); + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp new file mode 100644 index 0000000000..d62495d95e --- /dev/null +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -0,0 +1,237 @@ +// +// EntityEditFilters.cpp +// libraries/entities/src +// +// Created by David Kelly on 2/7/2017. +// 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 +// + + +#include + +#include +#include "EntityEditFilters.h" + +QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { + QList zones; + QList missingZones; + _lock.lockForRead(); + auto zoneIDs = _filterDataMap.keys(); + _lock.unlock(); + for (auto id : zoneIDs) { + if (!id.isInvalidID()) { + // for now, look it up in the tree (soon we need to cache or similar?) + EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id); + auto zone = std::dynamic_pointer_cast(itemPtr); + if (!zone) { + // TODO: maybe remove later? + removeFilter(id); + } else if (zone->contains(position)) { + zones.append(id); + } + } else { + // the null id is the global filter we put in the domain server's + // advanced entity server settings + zones.append(id); + } + } + return zones; +} + +bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, + EntityTree::FilterType filterType, EntityItemID& itemID) { + + // get the ids of all the zones (plus the global entity edit filter) that the position + // lies within + auto zoneIDs = getZonesByPosition(position); + for (auto id : zoneIDs) { + if (!itemID.isInvalidID() && id == itemID) { + continue; + } + + // get the filter pair, etc... + _lock.lockForRead(); + FilterData filterData = _filterDataMap.value(id); + _lock.unlock(); + + if (filterData.valid()) { + if (filterData.rejectAll) { + return false; + } + auto oldProperties = propertiesIn.getDesiredProperties(); + auto specifiedProperties = propertiesIn.getChangedProperties(); + propertiesIn.setDesiredProperties(specifiedProperties); + QScriptValue inputValues = propertiesIn.copyToScriptValue(filterData.engine, false, true, true); + propertiesIn.setDesiredProperties(oldProperties); + + auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. + QScriptValueList args; + args << inputValues; + args << filterType; + + QScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args); + if (filterData.uncaughtExceptions()) { + return false; + } + + if (result.isObject()){ + // make propertiesIn reflect the changes, for next filter... + propertiesIn.copyFromScriptValue(result, false); + + // and update propertiesOut too. TODO: this could be more efficient... + propertiesOut.copyFromScriptValue(result, false); + // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. + auto out = QJsonValue::fromVariant(result.toVariant()); + wasChanged |= (in != out); + } else { + return false; + } + } + } + // if we made it here, + return true; +} + +void EntityEditFilters::removeFilter(EntityItemID entityID) { + QWriteLocker writeLock(&_lock); + FilterData filterData = _filterDataMap.value(entityID); + if (filterData.valid()) { + delete filterData.engine; + } + _filterDataMap.remove(entityID); +} + +void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { + + QUrl scriptURL(filterURL); + + // setting it to an empty string is same as removing + if (filterURL.size() == 0) { + removeFilter(entityID); + return; + } + + // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) + if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { + qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; + scriptRequestFinished(entityID); + return; + } + + // first remove any existing info for this entity + removeFilter(entityID); + + // reject all edits until we load the script + FilterData filterData; + filterData.rejectAll = true; + + _lock.lockForWrite(); + _filterDataMap.insert(entityID, filterData); + _lock.unlock(); + + auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL); + if (!scriptRequest) { + qWarning() << "Could not create ResourceRequest for Entity Edit filter script at" << scriptURL.toString(); + scriptRequestFinished(entityID); + return; + } + // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own. + connect(scriptRequest, &ResourceRequest::finished, this, [this, entityID]{ EntityEditFilters::scriptRequestFinished(entityID);} ); + // FIXME: handle atp rquests setup here. See Agent::requestScript() + qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); + scriptRequest->send(); + qDebug() << "script request sent for entity " << entityID; +} + +// Copied from ScriptEngine.cpp. We should make this a class method for reuse. +// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point. +static bool hasCorrectSyntax(const QScriptProgram& program) { + const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); + if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { + const auto error = syntaxCheck.errorMessage(); + const auto line = QString::number(syntaxCheck.errorLineNumber()); + const auto column = QString::number(syntaxCheck.errorColumnNumber()); + const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); + qCritical() << qPrintable(message); + return false; + } + return true; +} +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; + auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qCritical() << qPrintable(message); + return true; + } + return false; +} + +void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { + qDebug() << "script request completed for entity " << entityID; + auto scriptRequest = qobject_cast(sender()); + const QString urlString = scriptRequest->getUrl().toString(); + if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { + auto scriptContents = scriptRequest->getData(); + qInfo() << "Downloaded script:" << scriptContents; + QScriptProgram program(scriptContents, urlString); + if (hasCorrectSyntax(program)) { + // create a QScriptEngine for this script + QScriptEngine* engine = new QScriptEngine(); + engine->evaluate(scriptContents); + if (!hadUncaughtExceptions(*engine, urlString)) { + // put the engine in the engine map (so we don't leak them, etc...) + FilterData filterData; + filterData.engine = engine; + filterData.rejectAll = false; + + // define the uncaughtException function + QScriptEngine& engineRef = *engine; + filterData.uncaughtExceptions = [this, &engineRef, urlString]() { return hadUncaughtExceptions(engineRef, urlString); }; + + // now get the filter function + auto global = engine->globalObject(); + auto entitiesObject = engine->newObject(); + entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add); + entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit); + entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics); + global.setProperty("Entities", entitiesObject); + filterData.filterFn = global.property("filter"); + if (!filterData.filterFn.isFunction()) { + qDebug() << "Filter function specified but not found. Will reject all edits for those without lock rights."; + delete engine; + filterData.rejectAll=true; + } + + + _lock.lockForWrite(); + _filterDataMap.insert(entityID, filterData); + _lock.unlock(); + + qDebug() << "script request filter processed for entity id " << entityID; + + emit filterAdded(entityID, true); + return; + } + } + } else if (scriptRequest) { + qCritical() << "Failed to download script at" << urlString; + // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. + qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); + } else { + qCritical() << "Failed to create script request."; + } + emit filterAdded(entityID, false); +} diff --git a/libraries/entities/src/EntityEditFilters.h b/libraries/entities/src/EntityEditFilters.h new file mode 100644 index 0000000000..6aeb347603 --- /dev/null +++ b/libraries/entities/src/EntityEditFilters.h @@ -0,0 +1,65 @@ +// +// EntityEditFilters.h +// libraries/entities/src +// +// Created by David Kelly on 2/7/2017. +// 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 +// +#ifndef hifi_EntityEditFilters_h +#define hifi_EntityEditFilters_h + +#include +#include +#include +#include +#include + +#include + +#include "EntityItemID.h" +#include "EntityItemProperties.h" +#include "EntityTree.h" + +class EntityEditFilters : public QObject, public Dependency { + Q_OBJECT +public: + struct FilterData { + QScriptValue filterFn; + std::function uncaughtExceptions; + QScriptEngine* engine; + bool rejectAll; + + FilterData(): engine(nullptr), rejectAll(false) {}; + bool valid() { return (rejectAll || (engine != nullptr && filterFn.isFunction() && uncaughtExceptions)); } + }; + + EntityEditFilters() {}; + EntityEditFilters(EntityTreePointer tree ): _tree(tree) {}; + + void addFilter(EntityItemID entityID, QString filterURL); + void removeFilter(EntityItemID entityID); + + bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, + EntityTree::FilterType filterType, EntityItemID& entityID); + +signals: + void filterAdded(EntityItemID id, bool success); + +private slots: + void scriptRequestFinished(EntityItemID entityID); + +private: + QList getZonesByPosition(glm::vec3& position); + + EntityTreePointer _tree {}; + bool _rejectAll {false}; + QScriptValue _nullObjectForFilter{}; + + QReadWriteLock _lock; + QMap _filterDataMap; +}; + +#endif //hifi_EntityEditFilters_h diff --git a/libraries/entities/src/EntityItemID.cpp b/libraries/entities/src/EntityItemID.cpp index 1462a4ef88..5f07019db4 100644 --- a/libraries/entities/src/EntityItemID.cpp +++ b/libraries/entities/src/EntityItemID.cpp @@ -19,6 +19,7 @@ #include "RegisteredMetaTypes.h" #include "EntityItemID.h" +int entityItemIDTypeID = qRegisterMetaType(); EntityItemID::EntityItemID() : QUuid() { diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 21018d8afa..ea81df3801 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -332,6 +332,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); + CHECK_PROPERTY_CHANGE(PROP_FILTER_URL, filterURL); CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); @@ -509,6 +510,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FILTER_URL, filterURL); } // Web only @@ -751,6 +753,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); + COPY_PROPERTY_FROM_QSCRIPTVALUE(filterURL, QString, setFilterURL); COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); @@ -879,6 +882,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(flyingAllowed); COPY_PROPERTY_IF_CHANGED(ghostingAllowed); + COPY_PROPERTY_IF_CHANGED(filterURL); COPY_PROPERTY_IF_CHANGED(clientOnly); COPY_PROPERTY_IF_CHANGED(owningAvatarID); @@ -1063,6 +1067,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool); ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool); + ADD_PROPERTY_TO_MAP(PROP_FILTER_URL, FilterURL, filterURL, QString); ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); @@ -1311,6 +1316,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, properties.getFlyingAllowed()); APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, properties.getGhostingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, properties.getFilterURL()); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1605,6 +1611,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FILTER_URL, QString, setFilterURL); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1808,6 +1815,7 @@ void EntityItemProperties::markAllChanged() { _flyingAllowedChanged = true; _ghostingAllowedChanged = true; + _filterURLChanged = true; _clientOnlyChanged = true; _owningAvatarIDChanged = true; @@ -2150,7 +2158,9 @@ QList EntityItemProperties::listChangedProperties() { if (ghostingAllowedChanged()) { out += "ghostingAllowed"; } - + if (filterURLChanged()) { + out += "filterURL"; + } if (dpiChanged()) { out += "dpi"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 1961feaf1d..419740e4ea 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -215,6 +215,7 @@ public: DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED); DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED); + DEFINE_PROPERTY(PROP_FILTER_URL, FilterURL, filterURL, QString, ZoneEntityItem::DEFAULT_FILTER_URL); DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false); DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID); @@ -458,6 +459,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, ""); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b77d3cc077..b3cfc143c2 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -185,6 +185,8 @@ enum EntityPropertyList { PROP_SERVER_SCRIPTS, + PROP_FILTER_URL, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 427f6b4af0..d7471474a6 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -24,6 +24,7 @@ #include "EntitiesLogging.h" #include "RecurseOctreeToMapOperator.h" #include "LogHandler.h" +#include "EntityEditFilters.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour @@ -923,55 +924,14 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList entityEditFilterHadUncaughtExceptions) { - _entityEditFilterEngine = engine; - _entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions; - auto global = _entityEditFilterEngine->globalObject(); - _entityEditFilterFunction = global.property("filter"); - if (!_entityEditFilterFunction.isFunction()) { - qCDebug(entities) << "Filter function specified but not found. Will reject all edits."; - _entityEditFilterEngine = nullptr; // So that we don't try to call it. See filterProperties. - } - auto entitiesObject = _entityEditFilterEngine->newObject(); - entitiesObject.setProperty("ADD_FILTER_TYPE", FilterType::Add); - entitiesObject.setProperty("EDIT_FILTER_TYPE", FilterType::Edit); - entitiesObject.setProperty("PHYSICS_FILTER_TYPE", FilterType::Physics); - global.setProperty("Entities", entitiesObject); - _hasEntityEditFilter = true; -} -bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) { - if (!_entityEditFilterEngine) { - propertiesOut = propertiesIn; - wasChanged = false; // not changed - if (_hasEntityEditFilter) { - qCDebug(entities) << "Rejecting properties because filter has not been set."; - return false; - } - return true; // allowed - } - auto oldProperties = propertiesIn.getDesiredProperties(); - auto specifiedProperties = propertiesIn.getChangedProperties(); - propertiesIn.setDesiredProperties(specifiedProperties); - QScriptValue inputValues = propertiesIn.copyToScriptValue(_entityEditFilterEngine, false, true, true); - propertiesIn.setDesiredProperties(oldProperties); - - auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. - QScriptValueList args; - args << inputValues; - args << filterType; - - QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args); - if (_entityEditFilterHadUncaughtExceptions()) { - result = QScriptValue(); - } - - bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add - if (accepted) { - propertiesOut.copyFromScriptValue(result, false); - // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. - auto out = QJsonValue::fromVariant(result.toVariant()); - wasChanged = in != out; +bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) { + bool accepted = true; + auto entityEditFilters = DependencyManager::get(); + if (entityEditFilters) { + auto position = existingEntity ? existingEntity->getPosition() : propertiesIn.getPosition(); + auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID(); + accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID); } return accepted; @@ -1076,11 +1036,16 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c // an existing entity... handle appropriately if (validEditPacket) { + // search for the entity by EntityItemID + startLookup = usecTimestampNow(); + EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID); + endLookup = usecTimestampNow(); + startFilter = usecTimestampNow(); bool wasChanged = false; // Having (un)lock rights bypasses the filter, unless it's a physics result. FilterType filterType = isPhysics ? FilterType::Physics : (isAdd ? FilterType::Add : FilterType::Edit); - bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(properties, properties, wasChanged, filterType); + bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(existingEntity, properties, properties, wasChanged, filterType); if (!allowed) { auto timestamp = properties.getLastEdited(); properties = EntityItemProperties(); @@ -1093,10 +1058,6 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } endFilter = usecTimestampNow(); - // search for the entity by EntityItemID - startLookup = usecTimestampNow(); - EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID); - endLookup = usecTimestampNow(); if (existingEntity && !isAdd) { if (suppressDisallowedScript) { @@ -1767,3 +1728,4 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { } return entity->getJointNames(); } + diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 5dad282d3b..63f7bbfd66 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -25,6 +25,7 @@ typedef std::shared_ptr EntityTreePointer; #include "EntityTreeElement.h" #include "DeleteEntityOperator.h" +class EntityEditFilters; class Model; using ModelPointer = std::shared_ptr; using ModelWeakPointer = std::weak_ptr; @@ -271,9 +272,6 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); - void initEntityEditFilterEngine(QScriptEngine* engine, std::function entityEditFilterHadUncaughtExceptions); - void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; } - static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; public slots: @@ -362,13 +360,8 @@ protected: float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; - bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); + bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); bool _hasEntityEditFilter{ false }; - QScriptEngine* _entityEditFilterEngine{}; - QScriptValue _entityEditFilterFunction{}; - QScriptValue _nullObjectForFilter{}; - std::function _entityEditFilterHadUncaughtExceptions; - QStringList _entityScriptSourceWhitelist; }; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 3e21497d63..37b3be99a3 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -19,6 +19,7 @@ #include "EntityTree.h" #include "EntityTreeElement.h" #include "ZoneEntityItem.h" +#include "EntityEditFilters.h" bool ZoneEntityItem::_zonesArePickable = false; bool ZoneEntityItem::_drawZoneBoundaries = false; @@ -28,7 +29,7 @@ const ShapeType ZoneEntityItem::DEFAULT_SHAPE_TYPE = SHAPE_TYPE_BOX; const QString ZoneEntityItem::DEFAULT_COMPOUND_SHAPE_URL = ""; const bool ZoneEntityItem::DEFAULT_FLYING_ALLOWED = true; const bool ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED = true; - +const QString ZoneEntityItem::DEFAULT_FILTER_URL = ""; EntityItemPointer ZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity { new ZoneEntityItem(entityID) }; @@ -61,6 +62,7 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed); COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(filterURL, getFilterURL); return properties; } @@ -79,6 +81,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL); bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties); @@ -128,6 +131,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); READ_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + READ_ENTITY_PROPERTY(PROP_FILTER_URL, QString, setFilterURL); return bytesRead; } @@ -147,6 +151,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p requestedProperties += PROP_FLYING_ALLOWED; requestedProperties += PROP_GHOSTING_ALLOWED; + requestedProperties += PROP_FILTER_URL; return requestedProperties; } @@ -177,6 +182,7 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, getFlyingAllowed()); APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, getGhostingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, getFilterURL()); } void ZoneEntityItem::debugDump() const { @@ -215,3 +221,13 @@ bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const return _zonesArePickable; } + +void ZoneEntityItem::setFilterURL(QString url) { + _filterURL = url; + if (DependencyManager::isSet()) { + auto entityEditFilters = DependencyManager::get(); + qCDebug(entities) << "adding filter " << url << "for zone" << getEntityItemID(); + entityEditFilters->addFilter(getEntityItemID(), url); + } +} + diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 3084d71f46..2bef95e452 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -74,6 +74,8 @@ public: void setFlyingAllowed(bool value) { _flyingAllowed = value; } bool getGhostingAllowed() const { return _ghostingAllowed; } void setGhostingAllowed(bool value) { _ghostingAllowed = value; } + QString getFilterURL() const { return _filterURL; } + void setFilterURL(const QString url); virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -87,6 +89,7 @@ public: static const QString DEFAULT_COMPOUND_SHAPE_URL; static const bool DEFAULT_FLYING_ALLOWED; static const bool DEFAULT_GHOSTING_ALLOWED; + static const QString DEFAULT_FILTER_URL; protected: KeyLightPropertyGroup _keyLightProperties; @@ -101,6 +104,7 @@ protected: bool _flyingAllowed { DEFAULT_FLYING_ALLOWED }; bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; + QString _filterURL { DEFAULT_FILTER_URL }; static bool _drawZoneBoundaries; static bool _zonesArePickable; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 855499c0e7..ddbc30d020 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_PHYSICS_PACKET; + return VERSION_ENTITIES_ZONE_FILTERS; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JsonFilter); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e198a486f7..de3d0369b5 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -204,6 +204,7 @@ const PacketVersion VERSION_ENTITIES_ARROW_ACTION = 64; const PacketVersion VERSION_ENTITIES_LAST_EDITED_BY = 65; const PacketVersion VERSION_ENTITIES_SERVER_SCRIPTS = 66; const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; +const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; enum class EntityQueryPacketVersion: PacketVersion { JsonFilter = 18 diff --git a/libraries/networking/src/udt/SendQueue.cpp b/libraries/networking/src/udt/SendQueue.cpp index 31d61bde5d..c14ae0a39c 100644 --- a/libraries/networking/src/udt/SendQueue.cpp +++ b/libraries/networking/src/udt/SendQueue.cpp @@ -478,6 +478,9 @@ bool SendQueue::maybeResendPacket() { Packet::ObfuscationLevel level = (Packet::ObfuscationLevel)(entry.first < 2 ? 0 : (entry.first - 2) % 4); + auto wireSize = resendPacket.getWireSize(); + auto sequenceNumber = it->first; + if (level != Packet::NoObfuscation) { #ifdef UDT_CONNECTION_DEBUG QString debugString = "Obfuscating packet %1 with level %2"; @@ -512,7 +515,7 @@ bool SendQueue::maybeResendPacket() { sentLocker.unlock(); } - emit packetRetransmitted(resendPacket.getWireSize(), it->first, p_high_resolution_clock::now()); + emit packetRetransmitted(wireSize, sequenceNumber, p_high_resolution_clock::now()); // Signal that we did resend a packet return true; diff --git a/libraries/script-engine/src/SoundEffect.cpp b/libraries/script-engine/src/SoundEffect.cpp index 1c78ae84bf..bfc0ad2100 100644 --- a/libraries/script-engine/src/SoundEffect.cpp +++ b/libraries/script-engine/src/SoundEffect.cpp @@ -5,9 +5,6 @@ #include SoundEffect::~SoundEffect() { - if (_sound) { - _sound->deleteLater(); - } if (_injector) { // stop will cause the AudioInjector to delete itself. _injector->stop(); diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index 7e8fdd6bc3..c78ce251c8 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -11,17 +11,36 @@ #include #include +#include "DependencyManager.h" #include +#include +#include #include #include "ScriptEngineLogging.h" -#include "DependencyManager.h" -#include "OffscreenUi.h" +#include +#include #include "SoundEffect.h" TabletScriptingInterface::TabletScriptingInterface() { qmlRegisterType("Hifi", 1, 0, "SoundEffect"); } +QObject* TabletScriptingInterface::getSystemToolbarProxy() { + const QString SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != _toolbarScriptingInterface->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + QObject* toolbarProxy = nullptr; + bool hasResult = QMetaObject::invokeMethod(_toolbarScriptingInterface, "getToolbar", connectionType, Q_RETURN_ARG(QObject*, toolbarProxy), Q_ARG(QString, SYSTEM_TOOLBAR)); + if (hasResult) { + return toolbarProxy; + } else { + qCWarning(scriptengine) << "ToolbarScriptingInterface getToolbar has no result"; + return nullptr; + } +} + QObject* TabletScriptingInterface::getTablet(const QString& tabletId) { std::lock_guard guard(_mutex); @@ -35,10 +54,21 @@ QObject* TabletScriptingInterface::getTablet(const QString& tabletId) { // allocate a new tablet, add it to the map then return it. auto tabletProxy = QSharedPointer(new TabletProxy(tabletId)); _tabletProxies[tabletId] = tabletProxy; + tabletProxy->setToolbarMode(_toolbarMode); return tabletProxy.data(); } } +void TabletScriptingInterface::setToolbarMode(bool toolbarMode) { + std::lock_guard guard(_mutex); + + _toolbarMode = toolbarMode; + + for (auto& iter : _tabletProxies) { + iter.second->setToolbarMode(toolbarMode); + } +} + void TabletScriptingInterface::setQmlTabletRoot(QString tabletId, QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface) { TabletProxy* tablet = qobject_cast(getTablet(tabletId)); if (tablet) { @@ -141,8 +171,51 @@ static const char* TABLET_SOURCE_URL = "Tablet.qml"; static const char* WEB_VIEW_SOURCE_URL = "TabletWebView.qml"; static const char* VRMENU_SOURCE_URL = "TabletMenu.qml"; +class TabletRootWindow : public QmlWindowClass { + virtual QString qmlSource() const { return "hifi/tablet/WindowRoot.qml"; } +}; + TabletProxy::TabletProxy(QString name) : _name(name) { - ; + +} + +void TabletProxy::setToolbarMode(bool toolbarMode) { + if (toolbarMode == _toolbarMode) { + return; + } + + _toolbarMode = toolbarMode; + + if (toolbarMode) { + removeButtonsFromHomeScreen(); + addButtonsToToolbar(); + + // create new desktop window + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + auto tabletRootWindow = new TabletRootWindow(); + tabletRootWindow->initQml(QVariantMap()); + auto quickItem = tabletRootWindow->asQuickItem(); + _desktopWindow = tabletRootWindow; + QMetaObject::invokeMethod(quickItem, "setShown", Q_ARG(const QVariant&, QVariant(false))); + + QObject::connect(quickItem, SIGNAL(windowClosed()), this, SLOT(desktopWindowClosed())); + + QObject::connect(tabletRootWindow, SIGNAL(webEventReceived(QVariant)), this, SIGNAL(webEventReceived(QVariant))); + + // forward qml surface events to interface js + connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); + }); + } else { + removeButtonsFromToolbar(); + addButtonsToHomeScreen(); + + // destroy desktop window + if (_desktopWindow) { + _desktopWindow->deleteLater(); + _desktopWindow = nullptr; + } + } } static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy* buttonProxy) { @@ -195,6 +268,13 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr } }); + if (_toolbarMode) { + // if someone creates the tablet in toolbar mode, make sure to display the home screen on the tablet. + auto loader = _qmlTabletRoot->findChild("loader"); + QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen()), Qt::DirectConnection); + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); + } + gotoHomeScreen(); QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); @@ -214,39 +294,61 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr } void TabletProxy::gotoMenuScreen(const QString& submenu) { - if (_qmlTabletRoot) { - if (_state != State::Menu) { - removeButtonsFromHomeScreen(); - QMetaObject::invokeMethod(_qmlTabletRoot, "setOption", Q_ARG(const QVariant&, QVariant(submenu))); - auto loader = _qmlTabletRoot->findChild("loader"); - QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen()), Qt::DirectConnection); - QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL))); - _state = State::Menu; - emit screenChanged(QVariant("Menu"), QVariant(VRMENU_SOURCE_URL)); - } + + QObject* root = nullptr; + if (!_toolbarMode && _qmlTabletRoot) { + root = _qmlTabletRoot; + } else if (_toolbarMode && _desktopWindow) { + root = _desktopWindow->asQuickItem(); + } + + if (root) { + removeButtonsFromHomeScreen(); + auto offscreenUi = DependencyManager::get(); + QObject* menu = offscreenUi->getRootMenu(); + QMetaObject::invokeMethod(root, "setMenuProperties", Q_ARG(QVariant, QVariant::fromValue(menu)), Q_ARG(const QVariant&, QVariant(submenu))); + QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL))); + _state = State::Menu; + emit screenChanged(QVariant("Menu"), QVariant(VRMENU_SOURCE_URL)); + QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } } void TabletProxy::loadQMLSource(const QVariant& path) { - if (_qmlTabletRoot) { + + QObject* root = nullptr; + if (!_toolbarMode && _qmlTabletRoot) { + root = _qmlTabletRoot; + } else if (_toolbarMode && _desktopWindow) { + root = _desktopWindow->asQuickItem(); + } + + if (root) { if (_state != State::QML) { removeButtonsFromHomeScreen(); - QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, path)); + QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); _state = State::QML; emit screenChanged(QVariant("QML"), path); + QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } } } + void TabletProxy::gotoHomeScreen() { - if (_qmlTabletRoot) { - if (_state != State::Home) { + if (_state != State::Home) { + if (!_toolbarMode && _qmlTabletRoot) { auto loader = _qmlTabletRoot->findChild("loader"); QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen()), Qt::DirectConnection); QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); QMetaObject::invokeMethod(_qmlTabletRoot, "playButtonClickSound"); - _state = State::Home; - emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); + } else if (_toolbarMode && _desktopWindow) { + // close desktop window + if (_desktopWindow->asQuickItem()) { + QMetaObject::invokeMethod(_desktopWindow->asQuickItem(), "setShown", Q_ARG(const QVariant&, QVariant(false))); + } } + _state = State::Home; + emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); } } @@ -255,31 +357,52 @@ void TabletProxy::gotoWebScreen(const QString& url) { } void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl) { - if (_qmlTabletRoot) { - if (_state == State::Home) { - removeButtonsFromHomeScreen(); - } - if (_state != State::Web) { - QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); - _state = State::Web; - emit screenChanged(QVariant("Web"), QVariant(url)); - } - QMetaObject::invokeMethod(_qmlTabletRoot, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), - Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); + + QObject* root = nullptr; + if (!_toolbarMode && _qmlTabletRoot) { + root = _qmlTabletRoot; + } else if (_toolbarMode && _desktopWindow) { + root = _desktopWindow->asQuickItem(); } + + if (root) { + QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); + QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); + QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); + } + _state = State::Web; + emit screenChanged(QVariant("Web"), QVariant(url)); } QObject* TabletProxy::addButton(const QVariant& properties) { auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); std::lock_guard guard(_mutex); _tabletButtonProxies.push_back(tabletButtonProxy); - if (_qmlTabletRoot) { + if (!_toolbarMode && _qmlTabletRoot) { auto tablet = getQmlTablet(); if (tablet) { addButtonProxyToQmlTablet(tablet, tabletButtonProxy.data()); } else { qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; } + } else if (_toolbarMode) { + + auto tabletScriptingInterface = DependencyManager::get(); + QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); + + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != toolbarProxy->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + + // copy properties from tablet button proxy to toolbar button proxy. + QObject* toolbarButtonProxy = nullptr; + bool hasResult = QMetaObject::invokeMethod(toolbarProxy, "addButton", connectionType, Q_RETURN_ARG(QObject*, toolbarButtonProxy), Q_ARG(QVariant, tabletButtonProxy->getProperties())); + if (hasResult) { + tabletButtonProxy->setToolbarButtonProxy(toolbarButtonProxy); + } else { + qCWarning(scriptengine) << "ToolbarProxy addButton has no result"; + } } return tabletButtonProxy.data(); } @@ -298,11 +421,18 @@ void TabletProxy::removeButton(QObject* tabletButtonProxy) { auto iter = std::find(_tabletButtonProxies.begin(), _tabletButtonProxies.end(), tabletButtonProxy); if (iter != _tabletButtonProxies.end()) { - if (_qmlTabletRoot) { + if (!_toolbarMode && _qmlTabletRoot) { (*iter)->setQmlButton(nullptr); if (tablet) { QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, (*iter)->getProperties())); } + } else if (_toolbarMode) { + auto tabletScriptingInterface = DependencyManager::get(); + QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); + + // remove button from toolbarProxy + QMetaObject::invokeMethod(toolbarProxy, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, (*iter)->getUuid().toString())); + (*iter)->setToolbarButtonProxy(nullptr); } _tabletButtonProxies.erase(iter); } else { @@ -329,20 +459,24 @@ void TabletProxy::updateAudioBar(const double micLevel) { } void TabletProxy::emitScriptEvent(QVariant msg) { - if (_qmlOffscreenSurface) { + if (!_toolbarMode && _qmlOffscreenSurface) { QMetaObject::invokeMethod(_qmlOffscreenSurface, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg)); + } else if (_toolbarMode && _desktopWindow) { + QMetaObject::invokeMethod(_desktopWindow, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg)); } } void TabletProxy::sendToQml(QVariant msg) { - if (_qmlOffscreenSurface) { + if (!_toolbarMode && _qmlOffscreenSurface) { QMetaObject::invokeMethod(_qmlOffscreenSurface, "sendToQml", Qt::AutoConnection, Q_ARG(QVariant, msg)); + } else if (_toolbarMode && _desktopWindow) { + QMetaObject::invokeMethod(_desktopWindow, "sendToQml", Qt::AutoConnection, Q_ARG(QVariant, msg)); } } void TabletProxy::addButtonsToHomeScreen() { auto tablet = getQmlTablet(); - if (!tablet) { + if (!tablet || _toolbarMode) { return; } @@ -358,30 +492,51 @@ QObject* TabletProxy::getTabletSurface() { return _qmlOffscreenSurface; } -void TabletProxy::addButtonsToMenuScreen() { - if (!_qmlTabletRoot) { - return; +void TabletProxy::removeButtonsFromHomeScreen() { + auto tablet = getQmlTablet(); + for (auto& buttonProxy : _tabletButtonProxies) { + if (tablet) { + QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getProperties())); + } + buttonProxy->setQmlButton(nullptr); } - - auto loader = _qmlTabletRoot->findChild("loader"); - if (!loader) { - return; - } - - QQuickItem* VrMenu = loader->findChild("tabletMenu"); - if (VrMenu) { - auto offscreenUi = DependencyManager::get(); - QObject* menu = offscreenUi->getRootMenu(); - QMetaObject::invokeMethod(VrMenu, "setRootMenu", Qt::AutoConnection, Q_ARG(QVariant, QVariant::fromValue(menu))); - } - - QObject::disconnect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen())); } -void TabletProxy::removeButtonsFromHomeScreen() { +void TabletProxy::desktopWindowClosed() { + gotoHomeScreen(); +} + +void TabletProxy::addButtonsToToolbar() { auto tabletScriptingInterface = DependencyManager::get(); + QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); + + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != toolbarProxy->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + for (auto& buttonProxy : _tabletButtonProxies) { - buttonProxy->setQmlButton(nullptr); + // copy properties from tablet button proxy to toolbar button proxy. + QObject* toolbarButtonProxy = nullptr; + bool hasResult = QMetaObject::invokeMethod(toolbarProxy, "addButton", connectionType, Q_RETURN_ARG(QObject*, toolbarButtonProxy), Q_ARG(QVariant, buttonProxy->getProperties())); + if (hasResult) { + buttonProxy->setToolbarButtonProxy(toolbarButtonProxy); + } else { + qCWarning(scriptengine) << "ToolbarProxy addButton has no result"; + } + } + + // make the toolbar visible + QMetaObject::invokeMethod(toolbarProxy, "writeProperty", Qt::AutoConnection, Q_ARG(QString, "visible"), Q_ARG(QVariant, QVariant(true))); +} + +void TabletProxy::removeButtonsFromToolbar() { + auto tabletScriptingInterface = DependencyManager::get(); + QObject* toolbarProxy = tabletScriptingInterface->getSystemToolbarProxy(); + for (auto& buttonProxy : _tabletButtonProxies) { + // remove button from toolbarProxy + QMetaObject::invokeMethod(toolbarProxy, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, buttonProxy->getUuid().toString())); + buttonProxy->setToolbarButtonProxy(nullptr); } } @@ -430,12 +585,14 @@ QQuickItem* TabletProxy::getQmlMenu() const { // const QString UUID_KEY = "uuid"; +const QString OBJECT_NAME_KEY = "objectName"; const QString STABLE_ORDER_KEY = "stableOrder"; static int s_stableOrder = 1; TabletButtonProxy::TabletButtonProxy(const QVariantMap& properties) : _uuid(QUuid::createUuid()), _stableOrder(++s_stableOrder), _properties(properties) { // this is used to uniquely identify this button. _properties[UUID_KEY] = _uuid; + _properties[OBJECT_NAME_KEY] = _uuid.toString(); _properties[STABLE_ORDER_KEY] = _stableOrder; } @@ -444,6 +601,14 @@ void TabletButtonProxy::setQmlButton(QQuickItem* qmlButton) { _qmlButton = qmlButton; } +void TabletButtonProxy::setToolbarButtonProxy(QObject* toolbarButtonProxy) { + std::lock_guard guard(_mutex); + _toolbarButtonProxy = toolbarButtonProxy; + if (_toolbarButtonProxy) { + QObject::connect(_toolbarButtonProxy, SIGNAL(clicked()), this, SLOT(clickedSlot())); + } +} + QVariantMap TabletButtonProxy::getProperties() const { std::lock_guard guard(_mutex); return _properties; @@ -451,6 +616,7 @@ QVariantMap TabletButtonProxy::getProperties() const { void TabletButtonProxy::editProperties(QVariantMap properties) { std::lock_guard guard(_mutex); + QVariantMap::const_iterator iter = properties.constBegin(); while (iter != properties.constEnd()) { _properties[iter.key()] = iter.value(); @@ -459,6 +625,10 @@ void TabletButtonProxy::editProperties(QVariantMap properties) { } ++iter; } + + if (_toolbarButtonProxy) { + QMetaObject::invokeMethod(_toolbarButtonProxy, "editProperties", Qt::AutoConnection, Q_ARG(QVariantMap, properties)); + } } #include "TabletScriptingInterface.moc" diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index 8ba69ccdde..e450923758 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -26,6 +26,7 @@ class TabletProxy; class TabletButtonProxy; +class QmlWindowClass; /**jsdoc * @namespace Tablet @@ -35,6 +36,9 @@ class TabletScriptingInterface : public QObject, public Dependency { public: TabletScriptingInterface(); + void setToolbarScriptingInterface(QObject* toolbarScriptingInterface) { _toolbarScriptingInterface = toolbarScriptingInterface; } + QObject* getSystemToolbarProxy(); + /**jsdoc * Creates or retruns a new TabletProxy and returns it. * @function Tablet.getTablet @@ -43,6 +47,8 @@ public: */ Q_INVOKABLE QObject* getTablet(const QString& tabletId); + void setToolbarMode(bool toolbarMode); + void setQmlTabletRoot(QString tabletId, QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface); void processEvent(const QKeyEvent* event); @@ -58,15 +64,20 @@ private: protected: std::mutex _mutex; std::map> _tabletProxies; + QObject* _toolbarScriptingInterface { nullptr }; + bool _toolbarMode { false }; }; /**jsdoc * @class TabletProxy * @property name {string} READ_ONLY: name of this tablet + * @property toolbarMode {bool} - used to transition this tablet into and out of toolbar mode. + * When tablet is in toolbar mode, all its buttons will appear in a floating toolbar. */ class TabletProxy : public QObject { Q_OBJECT Q_PROPERTY(QString name READ getName) + Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode) public: TabletProxy(QString name); @@ -74,6 +85,11 @@ public: Q_INVOKABLE void gotoMenuScreen(const QString& submenu = ""); + QString getName() const { return _name; } + + bool getToolbarMode() const { return _toolbarMode; } + void setToolbarMode(bool toolbarMode); + /**jsdoc * transition to the home screen * @function TabletProxy#gotoHomeScreen @@ -120,8 +136,6 @@ public: */ Q_INVOKABLE void updateAudioBar(const double micLevel); - QString getName() const { return _name; } - /**jsdoc * Used to send an event to the html/js embedded in the tablet * @function TabletProxy#emitScriptEvent @@ -162,24 +176,28 @@ signals: void fromQml(QVariant msg); /**jsdoc - * Signales when this tablet screen changes. + * Signaled when this tablet screen changes. * @function TabletProxy#screenChanged * @param type {string} - "Home", "Web", "Menu", "QML", "Closed" * @param url {string} - only valid for Web and QML. */ void screenChanged(QVariant type, QVariant url); -private slots: +protected slots: void addButtonsToHomeScreen(); - void addButtonsToMenuScreen(); + void desktopWindowClosed(); protected: void removeButtonsFromHomeScreen(); + void addButtonsToToolbar(); + void removeButtonsFromToolbar(); QString _name; std::mutex _mutex; std::vector> _tabletButtonProxies; QQuickItem* _qmlTabletRoot { nullptr }; QObject* _qmlOffscreenSurface { nullptr }; + QmlWindowClass* _desktopWindow { nullptr }; + bool _toolbarMode { false }; enum class State { Uninitialized, Home, Web, Menu, QML }; State _state { State::Uninitialized }; @@ -196,6 +214,7 @@ public: TabletButtonProxy(const QVariantMap& properties); void setQmlButton(QQuickItem* qmlButton); + void setToolbarButtonProxy(QObject* toolbarButtonProxy); QUuid getUuid() const { return _uuid; } @@ -229,6 +248,7 @@ protected: int _stableOrder; mutable std::mutex _mutex; QQuickItem* _qmlButton { nullptr }; + QObject* _toolbarButtonProxy { nullptr }; QVariantMap _properties; }; diff --git a/libraries/ui/src/InfoView.cpp b/libraries/ui/src/InfoView.cpp index d2c72bf5f2..cb80e3f6db 100644 --- a/libraries/ui/src/InfoView.cpp +++ b/libraries/ui/src/InfoView.cpp @@ -20,17 +20,22 @@ const QString InfoView::NAME{ "InfoView" }; Setting::Handle infoVersion("info-version", QString()); -InfoView::InfoView(QQuickItem* parent) : QQuickItem(parent) { +static bool registered{ false }; +InfoView::InfoView(QQuickItem* parent) : QQuickItem(parent) { + registerType(); } -void InfoView::registerType() { - qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); -} +void InfoView::registerType() { + if (!registered) { + qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); + registered = true; + } +} QString fetchVersion(const QUrl& url) { QXmlQuery query; - query.bindVariable("file", QVariant(url)); + query.bindVariable("file", QVariant(url)); query.setQuery("string((doc($file)//input[@id='version'])[1]/@value)"); QString r; query.evaluateTo(&r); @@ -38,14 +43,10 @@ QString fetchVersion(const QUrl& url) { } void InfoView::show(const QString& path, bool firstOrChangedOnly, QString urlQuery) { - static bool registered{ false }; - if (!registered) { - registerType(); - registered = true; - } + registerType(); QUrl url; if (QDir(path).isRelative()) { - url = QUrl::fromLocalFile(PathUtils::resourcesPath() + path); + url = QUrl::fromLocalFile(PathUtils::resourcesPath() + path); } else { url = QUrl::fromLocalFile(path); } @@ -56,7 +57,7 @@ void InfoView::show(const QString& path, bool firstOrChangedOnly, QString urlQue const QString version = fetchVersion(url); // If we have version information stored if (lastVersion != QString::null) { - // Check to see the document version. If it's valid and matches + // Check to see the document version. If it's valid and matches // the stored version, we're done, so exit if (version == QString::null || version == lastVersion) { return; @@ -87,4 +88,3 @@ void InfoView::setUrl(const QUrl& url) { emit urlChanged(); } } - diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index a6f59104fd..95777718bf 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -31,6 +31,9 @@ public: QmlWindowClass(); ~QmlWindowClass(); + virtual void initQml(QVariantMap properties); + QQuickItem* asQuickItem() const; + public slots: bool isVisible() const; void setVisible(bool visible); @@ -81,9 +84,6 @@ protected: virtual QString qmlSource() const { return "QmlWindow.qml"; } - virtual void initQml(QVariantMap properties); - QQuickItem* asQuickItem() const; - // FIXME needs to be initialized in the ctor once we have support // for tool window panes in QML bool _toolWindow { false }; diff --git a/scripts/system/audio.js b/scripts/system/audio.js index dd49f944ea..c0fdb43b40 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -9,49 +9,30 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE -var button; -var TOOLBAR_BUTTON_NAME = "MUTE"; var TABLET_BUTTON_NAME = "AUDIO"; -var toolBar = null; -var tablet = null; -var isHUDUIEnabled = Settings.getValue("HUDUIEnabled"); var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; function onMuteToggled() { - if (isHUDUIEnabled) { - button.editProperties({ isActive: AudioDevice.getMuted() }); - } + button.editProperties({ isActive: AudioDevice.getMuted() }); } function onClicked(){ - if (isHUDUIEnabled) { - var menuItem = "Mute Microphone"; - Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); - } else { - var entity = HMD.tabletID; - Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); - tablet.gotoMenuScreen("Audio"); - } + var entity = HMD.tabletID; + Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + tablet.gotoMenuScreen("Audio"); } -if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: TOOLBAR_BUTTON_NAME, - imageURL: Script.resolvePath("assets/images/tools/mic.svg"), - visible: true, - alpha: 0.9 - }); -} else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/mic-i.svg", - text: TABLET_BUTTON_NAME, - sortOrder: 1 - }); -} +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var button = tablet.addButton({ + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-mute-a.svg", + text: TABLET_BUTTON_NAME, + sortOrder: 1 +}); + onMuteToggled(); button.clicked.connect(onClicked); @@ -60,12 +41,7 @@ AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); AudioDevice.muteToggled.disconnect(onMuteToggled); - if (tablet) { - tablet.removeButton(button); - } - if (toolBar) { - toolBar.removeButton(TOOLBAR_BUTTON_NAME); - } + tablet.removeButton(button); }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index ff262e3d6e..8d103c93de 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -10,11 +10,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ - +/* global Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */ (function () { // BEGIN LOCAL_SCOPE - var button; // Used for animating and disappearing the bubble var bubbleOverlayTimestamp; @@ -23,7 +21,7 @@ // Used for flashing the HUD button upon activation var bubbleButtonTimestamp; // Affects bubble height - const BUBBLE_HEIGHT_SCALE = 0.15; + var BUBBLE_HEIGHT_SCALE = 0.15; // The bubble model itself var bubbleOverlay = Overlays.addOverlay("model", { url: Script.resolvePath("assets/models/Bubble-v14.fbx"), // If you'd like to change the model, modify this line (and the dimensions below) @@ -39,16 +37,8 @@ // Is the update() function connected? var updateConnected = false; - const BUBBLE_VISIBLE_DURATION_MS = 3000; - const BUBBLE_RAISE_ANIMATION_DURATION_MS = 750; - const BUBBLE_HUD_ICON_FLASH_INTERVAL_MS = 500; - - var ASSETS_PATH = Script.resolvePath("assets"); - var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); - - function buttonImageURL() { - return TOOLS_PATH + 'bubble.svg'; - } + var BUBBLE_VISIBLE_DURATION_MS = 3000; + var BUBBLE_RAISE_ANIMATION_DURATION_MS = 750; // Hides the bubble model overlay and resets the button flash state function hideOverlays() { @@ -94,7 +84,7 @@ } // The bubble script's update function - update = function () { + function update() { var timestamp = Date.now(); var delay = (timestamp - bubbleOverlayTimestamp); var overlayAlpha = 1.0 - (delay / BUBBLE_VISIBLE_DURATION_MS); @@ -146,7 +136,7 @@ var bubbleActive = Users.getIgnoreRadiusEnabled(); writeButtonProperties(bubbleActive); } - }; + } // When the space bubble is toggled... function onBubbleToggled() { @@ -165,38 +155,26 @@ // Setup the bubble button var buttonName = "BUBBLE"; - if (Settings.getValue("HUDUIEnabled")) { - var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolbar.addButton({ - objectName: 'bubble', - imageURL: buttonImageURL(), - visible: true, - alpha: 0.9 - }); - } else { - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/bubble-i.svg", - activeIcon: "icons/tablet-icons/bubble-a.svg", - text: buttonName, - sortOrder: 4 - }); - } + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/bubble-i.svg", + activeIcon: "icons/tablet-icons/bubble-a.svg", + text: buttonName, + sortOrder: 4 + }); + onBubbleToggled(); button.clicked.connect(Users.toggleIgnoreRadius); Users.ignoreRadiusEnabledChanged.connect(onBubbleToggled); Users.enteredIgnoreRadius.connect(enteredIgnoreRadius); - // Cleanup the toolbar button and overlays when script is stopped + // Cleanup the tablet button and overlays when script is stopped Script.scriptEnding.connect(function () { button.clicked.disconnect(Users.toggleIgnoreRadius); if (tablet) { tablet.removeButton(button); } - if (toolbar) { - toolbar.removeButton('bubble'); - } Users.ignoreRadiusEnabledChanged.disconnect(onBubbleToggled); Users.enteredIgnoreRadius.disconnect(enteredIgnoreRadius); Overlays.deleteOverlay(bubbleOverlay); diff --git a/scripts/system/help.js b/scripts/system/help.js index 4e7788a758..5a1b712fb5 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -10,48 +10,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Toolbars, Script, HMD, Controller, Menu */ +/* globals Tablet, Script, HMD, Controller, Menu */ (function() { // BEGIN LOCAL_SCOPE - var button; var buttonName = "HELP"; - var toolBar = null; - var tablet = null; - if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/help.svg"), - visible: true, - alpha: 0.9 - }); - } else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/help-i.svg", - activeIcon: "icons/tablet-icons/help-a.svg", - text: buttonName, - sortOrder: 6 - }); - } + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/help-i.svg", + activeIcon: "icons/tablet-icons/help-a.svg", + text: buttonName, + sortOrder: 6 + }); + var enabled = false; function onClicked() { - // Similar logic to Application::showHelp() - var defaultTab = "kbm"; - var handControllerName = "vive"; - if (HMD.active) { - if ("Vive" in Controller.Hardware) { - defaultTab = "handControllers"; - handControllerName = "vive"; - } else if ("OculusTouch" in Controller.Hardware) { - defaultTab = "handControllers"; - handControllerName = "oculus"; - } - } else if ("SDL2" in Controller.Hardware) { - defaultTab = "gamepad"; - } - if (enabled) { Menu.closeInfoView('InfoView_html/help.html'); enabled = !enabled; @@ -80,9 +53,6 @@ if (tablet) { tablet.removeButton(button); } - if (toolBar) { - toolBar.removeButton(buttonName); - } }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 3493215ba3..c206a76e3f 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -10,7 +10,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/*globals HMD, Toolbars, Script, Menu, Tablet, Camera */ +/* globals HMD, Script, Menu, Tablet, Camera */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -37,20 +38,13 @@ function updateControllerDisplay() { } var button; -var toolBar = null; -var tablet = null; - -if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); -} else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -} +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // Independent and Entity mode make people sick. Third Person and Mirror have traps that we need to work through. // Disable them in hmd. var desktopOnlyViews = ['Mirror', 'Independent Mode', 'Entity Mode']; + function onHmdChanged(isHmd) { - //TODO change button icon when the hmd changes if (isHmd) { button.editProperties({ icon: "icons/tablet-icons/switch-desk-i.svg", @@ -67,25 +61,18 @@ function onHmdChanged(isHmd) { }); updateControllerDisplay(); } -function onClicked(){ + +function onClicked() { var isDesktop = Menu.isOptionChecked(desktopMenuItemName); Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); } + if (headset) { - if (Settings.getValue("HUDUIEnabled")) { - button = toolBar.addButton({ - objectName: "hmdToggle", - imageURL: Script.resolvePath("assets/images/tools/switch.svg"), - visible: true, - alpha: 0.9 - }); - } else { - button = tablet.addButton({ - icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", - text: HMD.active ? "DESKTOP" : "VR", - sortOrder: 2 - }); - } + button = tablet.addButton({ + icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", + text: HMD.active ? "DESKTOP" : "VR", + sortOrder: 2 + }); onHmdChanged(HMD.active); button.clicked.connect(onClicked); @@ -97,9 +84,6 @@ if (headset) { if (tablet) { tablet.removeButton(button); } - if (toolBar) { - toolBar.removeButton("hmdToggle"); - } HMD.displayModeChanged.disconnect(onHmdChanged); Camera.modeUpdated.disconnect(updateControllerDisplay); }); diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index e563758782..b11127b26c 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -450,6 +450,11 @@ +
+
+ + +
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 957cea4528..8879c0f34e 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -697,6 +697,7 @@ function loaded() { var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); + var elZoneFilterURL = document.getElementById("property-zone-filter-url"); var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); allSections.push(elPolyVoxSections); @@ -1032,6 +1033,7 @@ function loaded() { elZoneFlyingAllowed.checked = properties.flyingAllowed; elZoneGhostingAllowed.checked = properties.ghostingAllowed; + elZoneFilterURL.value = properties.filterURL; showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); } else if (properties.type == "PolyVox") { @@ -1387,7 +1389,8 @@ function loaded() { elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); - + elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 0803f753c7..c5ce5a634b 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Tablet, Script, HMD, Toolbars, UserActivityLogger, Entities */ +/* global Tablet, Script, HMD, UserActivityLogger, Entities */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -33,8 +33,6 @@ 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 marketplaceWindow = null; - var CLARA_DOWNLOAD_TITLE = "Preparing Download"; var messageBox = null; var isDownloadBeingCancelled = false; @@ -57,52 +55,47 @@ Window.messageBoxClosed.connect(onMessageBoxClosed); function showMarketplace() { UserActivityLogger.openedMarketplace(); - if (tablet) { - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - tablet.webEventReceived.connect(function (message) { - if (message === GOTO_DIRECTORY) { - tablet.gotoWebScreen(MARKETPLACES_URL); - } + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + tablet.webEventReceived.connect(function (message) { - if (message === QUERY_CAN_WRITE_ASSETS) { - tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); - } + if (message === GOTO_DIRECTORY) { + tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } - if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - } + if (message === QUERY_CAN_WRITE_ASSETS) { + tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } - if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { - return; - } + if (message === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } + if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { return; } - if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; - } - return; + var text = message.slice(CLARA_IO_STATUS.length); + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } else { + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } + return; + } - if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; + if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; } - }); - } else { - marketplaceWindow.setURL(MARKETPLACE_URL_INITIAL); - marketplaceWindow.setVisible(true); - marketplaceVisible = true; - } + return; + } + + if (message === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; + } + }); } function toggleMarketplace() { @@ -111,33 +104,12 @@ function toggleMarketplace() { showMarketplace(); } -var tablet = null; -var toolBar = null; -var marketplaceButton = null; -if (Settings.getValue("HUDUIEnabled")) { - marketplaceWindow = new OverlayWebWindow({ - title: "Marketplace", - source: "about:blank", - width: 900, - height: 700, - visible: false - }); - marketplaceWindow.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL); - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - var toolIconUrl = Script.resolvePath("../assets/images/tools/"); - marketplaceButton = toolBar.addButton({ - imageURL: toolIconUrl + "market.svg", - objectName: "marketplace", - alpha: 0.9 - }); -} else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - marketplaceButton = tablet.addButton({ - icon: "icons/tablet-icons/market-i.svg", - text: "MARKET", - sortOrder: 9 - }); -} +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var marketplaceButton = tablet.addButton({ + icon: "icons/tablet-icons/market-i.svg", + text: "MARKET", + sortOrder: 9 +}); function onCanWriteAssetsChanged() { var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); @@ -152,9 +124,6 @@ marketplaceButton.clicked.connect(onClick); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); Script.scriptEnding.connect(function () { - if (toolBar) { - toolBar.removeButton("marketplace"); - } if (tablet) { tablet.removeButton(marketplaceButton); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 2e07a2d431..d47544e0f0 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,7 @@ "use strict"; -/*jslint vars: true, plusplus: true, forin: true*/ -/*globals Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, OverlayWindow, Toolbars, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js // @@ -13,21 +14,24 @@ (function() { // BEGIN LOCAL_SCOPE -// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed +// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // something, will revisit as this is sorta horrible. -const UNSELECTED_TEXTURES = {"idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") +var UNSELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") }; -const SELECTED_TEXTURES = { "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") +var SELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") }; -const HOVER_TEXTURES = { "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"), - "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png") +var HOVER_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png") }; -const UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; -const SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; -const HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now +var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; +var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; +var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now var conserveResources = true; @@ -87,24 +91,24 @@ ExtendedOverlay.prototype.hover = function (hovering) { } else { lastHoveringId = 0; } - } + } this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); if (this.model) { this.model.editOverlay({textures: textures(this.selected, hovering)}); } if (hovering) { // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId != this.key) { + if (lastHoveringId && lastHoveringId !== this.key) { ExtendedOverlay.get(lastHoveringId).hover(false); } lastHoveringId = this.key; } -} +}; ExtendedOverlay.prototype.select = function (selected) { if (this.selected === selected) { return; } - + UserActivityLogger.palAction(selected ? "avatar_selected" : "avatar_deselected", this.key); this.editOverlay({color: color(selected, this.hovering, this.audioLevel)}); @@ -193,17 +197,8 @@ HighlightedEntity.updateOverlays = function updateHighlightedEntities() { }); }; -// -// The qml window and communications. -// -var pal = new OverlayWindow({ - title: 'People Action List', - source: 'hifi/Pal.qml', - width: 580, - height: 640, - visible: false -}); function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + var data; switch (message.method) { case 'selected': selectedIds = message.params; @@ -250,7 +245,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } break; case 'displayNameUpdate': - if (MyAvatar.displayName != message.params) { + if (MyAvatar.displayName !== message.params) { MyAvatar.displayName = message.params; UserActivityLogger.palAction("display_name_change", ""); } @@ -261,11 +256,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } function sendToQml(message) { - if (Settings.getValue("HUDUIEnabled")) { - pal.sendToQml(message); - } else { - tablet.sendToQml(message); - } + tablet.sendToQml(message); } // @@ -273,7 +264,7 @@ function sendToQml(message) { // function addAvatarNode(id) { var selected = ExtendedOverlay.isSelected(id); - return new ExtendedOverlay(id, "sphere", { + return new ExtendedOverlay(id, "sphere", { drawInFront: true, solid: true, alpha: 0.8, @@ -290,17 +281,14 @@ function populateUserList(selectData) { userName: '', sessionId: id || '', audioLevel: 0.0, - admin: false + admin: false, + personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null + ignore: !!id && Users.getIgnoreStatus(id) // ditto }; - // Request the username, fingerprint, and admin status from the given UUID - // Username and fingerprint returns default constructor output if the requesting user isn't an admin - Users.requestUsernameFromID(id); - // Request personal mute status and ignore status - // from NodeList (as long as we're not requesting it for our own ID) if (id) { - avatarPalDatum['personalMute'] = Users.getPersonalMuteStatus(id); - avatarPalDatum['ignore'] = Users.getIgnoreStatus(id); addAvatarNode(id); // No overlay for ourselves + // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. + Users.requestUsernameFromID(id); } data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); @@ -314,20 +302,13 @@ function populateUserList(selectData) { // The function that handles the reply from the server function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - var data; - // If the ID we've received is our ID... - if (MyAvatar.sessionUUID === id) { - // Set the data to contain specific strings. - data = ['', username, isAdmin]; - } else if (Users.canKick) { - // Set the data to contain the ID and the username (if we have one) - // or fingerprint (if we don't have a username) string. - data = [id, username || machineFingerprint, isAdmin]; - } else { - // Set the data to contain specific strings. - data = [id, '', isAdmin]; - } - print('Username Data:', JSON.stringify(data)); + var data = [ + (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. + // If we get username (e.g., if in future we receive it when we're friends), use it. + // Otherwise, use valid machineFingerprint (which is not valid when not an admin). + username || (Users.canKick && machineFingerprint) || '', + isAdmin + ]; // Ship the data off to QML sendToQml({ method: 'updateUsername', params: data }); } @@ -339,17 +320,19 @@ function updateOverlays() { if (!id) { return; // don't update ourself } - + var avatar = AvatarList.getAvatar(id); + if (!avatar) { + return; // will be deleted below if there had been an overlay. + } var overlay = ExtendedOverlay.get(id); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. print('Adding non-PAL avatar node', id); overlay = addAvatarNode(id); } - var avatar = AvatarList.getAvatar(id); var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; - + // base offset on 1/2 distance from hips to head if we can var headIndex = avatar.getJointIndex("Head"); if (headIndex > 0) { @@ -358,7 +341,7 @@ function updateOverlays() { // get diff between target and eye (a vector pointing to the eye from avatar position) var diff = Vec3.subtract(target, eye); - + // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -369,12 +352,12 @@ function updateOverlays() { overlay.editOverlay({ color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), position: target, - dimensions: 0.032 * distance + dimensions: 0.032 * distance }); if (overlay.model) { overlay.model.ping = pingPong; overlay.model.editOverlay({ - position: target, + position: target, scale: 0.2 * distance, // constant apparent size rotation: Camera.orientation }); @@ -393,7 +376,9 @@ function removeOverlays() { selectedIds = []; lastHoveringId = 0; HighlightedEntity.clearOverlays(); - ExtendedOverlay.some(function (overlay) { overlay.deleteOverlay(); }); + ExtendedOverlay.some(function (overlay) { + overlay.deleteOverlay(); + }); } // @@ -423,12 +408,13 @@ function handleMouseMove(pickRay) { // given the pickRay, just do the hover logi // handy global to keep track of which hand is the mouse (if any) var currentHandPressed = 0; -const TRIGGER_CLICK_THRESHOLD = 0.85; -const TRIGGER_PRESS_THRESHOLD = 0.05; +var TRIGGER_CLICK_THRESHOLD = 0.85; +var TRIGGER_PRESS_THRESHOLD = 0.05; function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position + var pickRay; if (HMD.active) { - if (currentHandPressed != 0) { + if (currentHandPressed !== 0) { pickRay = controllerComputePickRay(currentHandPressed); } else { // nothing should hover, so @@ -441,18 +427,18 @@ function handleMouseMoveEvent(event) { // find out which overlay (if any) is ove handleMouseMove(pickRay); } function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one + // The idea is if you press one trigger, it is the one // we will consider the mouse. Even if the other is pressed, // we ignore it until this one is no longer pressed. - isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed == 0) { + var isPressed = value > TRIGGER_PRESS_THRESHOLD; + if (currentHandPressed === 0) { currentHandPressed = isPressed ? hand : 0; return; } - if (currentHandPressed == hand) { + if (currentHandPressed === hand) { currentHandPressed = isPressed ? hand : 0; return; - } + } // otherwise, the other hand is still triggered // so do nothing. } @@ -478,7 +464,7 @@ function makeClickHandler(hand) { function makePressHandler(hand) { return function (value) { handleTriggerPressed(hand, value); - } + }; } triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); @@ -490,17 +476,14 @@ triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Cont var button; var buttonName = "PEOPLE"; var tablet = null; -var toolBar = null; -if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/people.svg"), - visible: true, - alpha: 0.9 - }); - pal.fromQml.connect(fromQml); -} else { + +function onTabletScreenChanged(type, url) { + if (type !== "QML" || url !== "../Pal.qml") { + off(); + } +} + +function startup() { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ text: buttonName, @@ -508,8 +491,19 @@ if (Settings.getValue("HUDUIEnabled")) { sortOrder: 7 }); tablet.fromQml.connect(fromQml); + button.clicked.connect(onTabletButtonClicked); + tablet.screenChanged.connect(onTabletScreenChanged); + + Users.usernameFromIDReply.connect(usernameFromIDReply); + Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); + Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); + Messages.subscribe(CHANNEL); + Messages.messageReceived.connect(receiveMessage); + Users.avatarDisconnected.connect(avatarDisconnected); } +startup(); + var isWired = false; var audioTimer; var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) @@ -521,41 +515,26 @@ function off() { Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); isWired = false; } - if (audioTimer) { Script.clearInterval(audioTimer); } + if (audioTimer) { + Script.clearInterval(audioTimer); + } triggerMapping.disable(); // It's ok if we disable twice. triggerPressMapping.disable(); // see above removeOverlays(); Users.requestsDomainListData = false; } -function onClicked() { - if (Settings.getValue("HUDUIEnabled")) { - if (!pal.visible) { - Users.requestsDomainListData = true; - populateUserList(); - pal.raise(); - isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); - } else { - off(); - } - pal.setVisible(!pal.visible); - } else { - tablet.loadQMLSource("../Pal.qml"); - Users.requestsDomainListData = true; - populateUserList(); - isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); - } + +function onTabletButtonClicked() { + tablet.loadQMLSource("../Pal.qml"); + Users.requestsDomainListData = true; + populateUserList(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); } // @@ -570,17 +549,12 @@ function receiveMessage(channel, messageString, senderID) { var message = JSON.parse(messageString); switch (message.method) { case 'select': - if (!pal.visible) { - onClicked(); - } sendToQml(message); // Accepts objects, not just strings. break; default: print('Unrecognized PAL message', messageString); } } -Messages.subscribe(CHANNEL); -Messages.messageReceived.connect(receiveMessage); var AVERAGING_RATIO = 0.05; @@ -638,57 +612,29 @@ function avatarDisconnected(nodeID) { // remove from the pal list sendToQml({method: 'avatarDisconnected', params: [nodeID]}); } -// -// Button state. -// -function onVisibleChanged() { - button.editProperties({isActive: pal.visible}); -} -button.clicked.connect(onClicked); -pal.visibleChanged.connect(onVisibleChanged); -pal.closed.connect(off); - -if (!Settings.getValue("HUDUIEnabled")) { - tablet.screenChanged.connect(function (type, url) { - if (type !== "QML" || url !== "../Pal.qml") { - off(); - } - }); -} - -Users.usernameFromIDReply.connect(usernameFromIDReply); -Users.avatarDisconnected.connect(avatarDisconnected); function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); - if (pal.visible) { - onClicked(); // Close the PAL - } } -Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); -Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); + +function shutdown() { + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + tablet.screenChanged.disconnect(onTabletScreenChanged); + + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); + Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); + Messages.subscribe(CHANNEL); + Messages.messageReceived.disconnect(receiveMessage); + Users.avatarDisconnected.disconnect(avatarDisconnected); + + off(); +} // // Cleanup. // -Script.scriptEnding.connect(function () { - button.clicked.disconnect(onClicked); - if (tablet) { - tablet.removeButton(button); - } - if (toolBar) { - toolBar.removeButton(buttonName); - } - pal.visibleChanged.disconnect(onVisibleChanged); - pal.closed.disconnect(off); - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); - Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); - Messages.unsubscribe(CHANNEL); - Messages.messageReceived.disconnect(receiveMessage); - Users.avatarDisconnected.disconnect(avatarDisconnected); - off(); -}); - +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index c9462bbe7f..8f918c9cb2 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,7 +7,8 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Toolbars, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */ +/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -17,29 +18,15 @@ var resetOverlays; var reticleVisible; var clearOverlayWhenMoving; -var button; var buttonName = "SNAP"; -var tablet = null; -var toolBar = null; - var buttonConnected = false; -if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/snap.svg"), - visible: true, - alpha: 0.9, - }); -} else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/snap-i.svg", - text: buttonName, - sortOrder: 5 - }); -} +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var button = tablet.addButton({ + icon: "icons/tablet-icons/snap-i.svg", + text: buttonName, + sortOrder: 5 +}); function shouldOpenFeedAfterShare() { var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false" @@ -63,42 +50,42 @@ function confirmShare(data) { var isLoggedIn; var needsLogin = false; switch (message) { - case 'ready': - dialog.emitScriptEvent(data); // Send it. - outstanding = 0; - break; - case 'openSettings': - Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); - break; - case 'setOpenFeedFalse': - Settings.setValue('openFeedAfterShare', false); - break; - case 'setOpenFeedTrue': - Settings.setValue('openFeedAfterShare', true); - break; - default: - dialog.webEventReceived.disconnect(onMessage); - dialog.close(); - isLoggedIn = Account.isLoggedIn(); - message.forEach(function (submessage) { - if (submessage.share && !isLoggedIn) { - needsLogin = true; - submessage.share = false; - } - if (submessage.share) { - print('sharing', submessage.localPath); - outstanding++; - Window.shareSnapshot(submessage.localPath, submessage.href); - } else { - print('not sharing', submessage.localPath); - } - }); - if (!outstanding && shouldOpenFeedAfterShare()) { - showFeedWindow(); + case 'ready': + dialog.emitScriptEvent(data); // Send it. + outstanding = 0; + break; + case 'openSettings': + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + break; + case 'setOpenFeedFalse': + Settings.setValue('openFeedAfterShare', false); + break; + case 'setOpenFeedTrue': + Settings.setValue('openFeedAfterShare', true); + break; + default: + dialog.webEventReceived.disconnect(onMessage); + dialog.close(); + isLoggedIn = Account.isLoggedIn(); + message.forEach(function (submessage) { + if (submessage.share && !isLoggedIn) { + needsLogin = true; + submessage.share = false; } - if (needsLogin) { // after the possible feed, so that the login is on top - Account.checkAndSignalForAccessToken(); + if (submessage.share) { + print('sharing', submessage.localPath); + outstanding++; + Window.shareSnapshot(submessage.localPath, submessage.href); + } else { + print('not sharing', submessage.localPath); } + }); + if (!outstanding && shouldOpenFeedAfterShare()) { + showFeedWindow(); + } + if (needsLogin) { // after the possible feed, so that the login is on top + Account.checkAndSignalForAccessToken(); + } } } dialog.webEventReceived.connect(onMessage); @@ -159,7 +146,7 @@ function isDomainOpen(id) { var url = location.metaverseServerUrl + "/api/v1/user_stories?" + options.join('&'); request.open("GET", url, false); request.send(); - if (request.status != 200) { + if (request.status !== 200) { return false; } var response = JSON.parse(request.response); // Not parsed for us. @@ -229,9 +216,6 @@ Script.scriptEnding.connect(function () { if (tablet) { tablet.removeButton(button); } - if (toolBar) { - toolBar.removeButton(buttonName); - } Window.snapshotShared.disconnect(snapshotShared); Window.processingGif.disconnect(processingGif); }); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 5283df6127..6c3e12cd9b 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -12,54 +12,27 @@ // (function() { // BEGIN LOCAL_SCOPE - var gotoQmlSource = "TabletAddressDialog.qml"; - var button; + var gotoQmlSource = "TabletAddressDialog.qml"; var buttonName = "GOTO"; - var toolBar = null; - var tablet = null; - function onAddressBarShown(visible) { - if (toolBar) { - button.editProperties({isActive: visible}); - } - } function onClicked(){ - if (toolBar) { - DialogsManager.toggleAddressBar(); - } else { - tablet.loadQMLSource(gotoQmlSource); - } + tablet.loadQMLSource(gotoQmlSource); } - if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/directory.svg"), - visible: true, - alpha: 0.9 - }); - } else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/goto-i.svg", - activeIcon: "icons/tablet-icons/goto-a.svg", - text: buttonName, - sortOrder: 8 - }); - } - + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", + text: buttonName, + sortOrder: 8 + }); + button.clicked.connect(onClicked); - DialogsManager.addressBarShown.connect(onAddressBarShown); - + Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); if (tablet) { tablet.removeButton(button); } - if (toolBar) { - toolBar.removeButton(buttonName); - } - DialogsManager.addressBarShown.disconnect(onAddressBarShown); }); - + }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 1dc6b7fef8..632cb40bb5 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -52,6 +52,15 @@ } function updateShowTablet() { + + // close the WebTablet if it we go into toolbar mode. + var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + if (tabletShown && toolbarMode) { + hideTabletUI(); + HMD.closeTablet(); + return; + } + if (tabletShown) { var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone"; var currentMicEnabled = !Menu.isOptionChecked(MUTE_MICROPHONE_MENU_ITEM); @@ -67,7 +76,7 @@ // other reason, close the tablet. hideTabletUI(); HMD.closeTablet(); - } else if (HMD.showTablet && !tabletShown) { + } else if (HMD.showTablet && !tabletShown && !toolbarMode) { UserActivityLogger.openedTablet(); showTabletUI(); } else if (!HMD.showTablet && tabletShown) {