diff --git a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml index 73819839a1..e2172d8eda 100644 --- a/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/CheckBoxPreference.qml @@ -34,7 +34,7 @@ Preference { left: parent.left right: parent.right } - height: isFirstCheckBox ? hifi.dimensions.controlInterlineHeight : 0 + height: isFirstCheckBox && !preference.indented ? 16 : 2 } CheckBox { @@ -54,6 +54,7 @@ Preference { left: parent.left right: parent.right bottom: parent.bottom + leftMargin: preference.indented ? 20 : 0 } text: root.label colorScheme: hifi.colorSchemes.dark diff --git a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml index 77c94c2ae5..103904a666 100644 --- a/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml +++ b/interface/resources/qml/dialogs/preferences/RadioButtonsPreference.qml @@ -11,14 +11,27 @@ import QtQuick 2.5 import "../../controls-uit" +import "../../styles-uit" Preference { id: root - + height: control.height + hifi.dimensions.controlInterlineHeight + property int value: 0 + Component.onCompleted: { - repeater.itemAt(preference.value).checked = true + value = preference.value; + repeater.itemAt(preference.value).checked = true; + } + + function updateValue() { + for (var i = 0; i < repeater.count; i++) { + if (repeater.itemAt(i).checked) { + value = i; + break; + } + } } function save() { @@ -33,24 +46,36 @@ Preference { preference.save(); } - Row { + Column { id: control anchors { left: parent.left right: parent.right bottom: parent.bottom } - spacing: 5 + spacing: 3 + + RalewaySemiBold { + id: heading + size: hifi.fontSizes.inputLabel + text: preference.heading + color: hifi.colors.lightGrayText + visible: text !== "" + bottomPadding: 3 + } Repeater { id: repeater model: preference.items.length delegate: RadioButton { text: preference.items[index] + letterSpacing: 0 anchors { - verticalCenter: parent.verticalCenter + left: parent.left } + leftPadding: 0 colorScheme: hifi.colorSchemes.dark + onClicked: updateValue(); } } } diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index 0284af9d9c..c2c6583b7e 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -138,11 +138,12 @@ Preference { break; case Preference.PrimaryHand: - checkBoxCount++; + checkBoxCount = 0; builder = primaryHandBuilder; break; + case Preference.RadioButtons: - checkBoxCount++; + checkBoxCount = 0; builder = radioButtonsBuilder; break; }; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index e8fc41da63..1384cb8711 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -48,7 +48,7 @@ Rectangle { HifiModels.PSFListModel { id: connectionsUserModel; http: http; - endpoint: "/api/v1/users?filter=connections"; + endpoint: "/api/v1/users/connections"; property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); sortProperty: switch (sortColumn && sortColumn.role) { case 'placeName': diff --git a/interface/resources/qml/hifi/avatarapp/+android/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/+android/TransparencyMask.qml new file mode 100644 index 0000000000..6c50a6093a --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/+android/TransparencyMask.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 + +Item { + property alias source: sourceImage.sourceItem + property alias maskSource: sourceMask.sourceItem + + anchors.fill: parent + ShaderEffectSource { + id: sourceMask + smooth: true + hideSource: true + } + ShaderEffectSource { + id: sourceImage + hideSource: true + } + + ShaderEffect { + id: maskEffect + anchors.fill: parent + + property variant source: sourceImage + property variant mask: sourceMask + + fragmentShader: { +" + varying highp vec2 qt_TexCoord0; + uniform lowp sampler2D source; + uniform lowp sampler2D mask; + void main() { + + highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + + if (maskColor.a > 0.0) + gl_FragColor = sourceColor; + else + gl_FragColor = maskColor; + } +" + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml index db9b4f06ae..c8ac5f4778 100644 --- a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml +++ b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml @@ -24,20 +24,20 @@ Item { fragmentShader: { " -#version 150 core - varying highp vec2 qt_TexCoord0; - uniform lowp sampler2D source; - uniform lowp sampler2D mask; - void main() { - - highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); - highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); - - if (maskColor.a > 0.0) - gl_FragColor = sourceColor; - else - gl_FragColor = maskColor; - } +#version 410 +in vec2 qt_TexCoord0; +out vec4 color; +uniform sampler2D source; +uniform sampler2D mask; +void main() +{ + vec4 maskColor = texture(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + vec4 sourceColor = texture(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + if (maskColor.a > 0.0) + color = sourceColor; + else + color = maskColor; +} " } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index ec2c003383..3708f75114 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -122,6 +122,22 @@ Item { } } + // Runtime customization of preferences. + var locomotionPreference = findPreference("VR Movement", "Teleporting only / Walking and teleporting"); + var flyingPreference = findPreference("VR Movement", "Jumping and flying"); + if (locomotionPreference && flyingPreference) { + flyingPreference.visible = (locomotionPreference.value === 1); + locomotionPreference.valueChanged.connect(function () { + flyingPreference.visible = (locomotionPreference.value === 1); + }); + } + if (HMD.isHeadControllerAvailable("Oculus")) { + var boundariesPreference = findPreference("VR Movement", "Show room boundaries while teleporting"); + if (boundariesPreference) { + boundariesPreference.label = "Show room boundaries and sensors while teleporting"; + } + } + if (sections.length) { // Default sections to expanded/collapsed as appropriate for dialog. if (sections.length === 1) { @@ -234,4 +250,32 @@ Item { } } } + + function findPreference(category, name) { + var section = null; + var preference = null; + var i; + + // Find category section. + i = 0; + while (!section && i < sections.length) { + if (sections[i].name === category) { + section = sections[i]; + } + i++; + } + + // Find named preference. + if (section) { + i = 0; + while (!preference && i < section.preferences.length) { + if (section.preferences[i].preference && section.preferences[i].preference.name === name) { + preference = section.preferences[i]; + } + i++; + } + } + + return preference; + } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 833175f311..6ac3f706e4 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -153,11 +153,12 @@ Preference { break; case Preference.PrimaryHand: - checkBoxCount++; + checkBoxCount = 0; builder = primaryHandBuilder; break; + case Preference.RadioButtons: - checkBoxCount++; + checkBoxCount = 0; builder = radioButtonsBuilder; break; }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2cbe996e3c..12ff9a1982 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -410,6 +410,10 @@ public: }); } + void setMainThreadID(Qt::HANDLE threadID) { + _mainThreadID = threadID; + } + static void updateHeartbeat() { auto now = usecTimestampNow(); auto elapsed = now - _heartbeat; @@ -417,7 +421,9 @@ public: _heartbeat = now; } - static void deadlockDetectionCrash() { + void deadlockDetectionCrash() { + setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); + setCrashAnnotation("deadlock", "1"); uint32_t* crashTrigger = nullptr; *crashTrigger = 0xDEAD10CC; } @@ -504,6 +510,8 @@ public: static ThreadSafeMovingAverage _movingAverage; bool _quit { false }; + + Qt::HANDLE _mainThreadID = nullptr; }; std::atomic DeadlockWatchdogThread::_paused; @@ -1053,6 +1061,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto controllerScriptingInterface = DependencyManager::get().data(); _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); + connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, + controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); _entityClipboard->createRootElement(); @@ -1092,7 +1102,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } // Set up a watchdog thread to intentionally crash the application on deadlocks if (!DISABLE_WATCHDOG) { - (new DeadlockWatchdogThread())->start(); + auto deadlockWatchdogThread = new DeadlockWatchdogThread(); + deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); + deadlockWatchdogThread->start(); } // Set File Logger Session UUID diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 497ea351a0..086cbb3425 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -106,6 +106,7 @@ MyAvatar::MyAvatar(QThread* thread) : _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", true), + _showPlayArea("showPlayArea", true), _smoothOrientationTimer(std::numeric_limits::max()), _smoothOrientationInitial(), _smoothOrientationTarget(), @@ -3307,7 +3308,7 @@ float MyAvatar::getRawDriveKey(DriveKeys key) const { } void MyAvatar::relayDriveKeysToCharacterController() { - if (getDriveKey(TRANSLATE_Y) > 0.0f) { + if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) { _characterController.jump(); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c861ee48a4..16b765711a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -122,8 +122,10 @@ class MyAvatar : public Avatar { * zone may disallow collisionless avatars. * @property {boolean} characterControllerEnabled - Synonym of collisionsEnabled. * Deprecated: Use collisionsEnabled instead. - * @property {boolean} useAdvancedMovementControls - Returns the value of the Interface setting, Settings > Advanced - * Movement for Hand Controller. Note: Setting the value has no effect unless Interface is restarted. + * @property {boolean} useAdvancedMovementControls - Returns and sets the value of the Interface setting, Settings > + * Walking and teleporting. Note: Setting the value has no effect unless Interface is restarted. + * @property {boolean} showPlayArea - Returns and sets the value of the Interface setting, Settings > Show room boundaries + * while teleporting. Note: Setting the value has no effect unless Interface is restarted. * @property {number} yawSpeed=75 * @property {number} pitchSpeed=50 * @property {boolean} hmdRollControlEnabled=true - If true, the roll angle of your HMD turns your avatar @@ -223,6 +225,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) + Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea) Q_PROPERTY(float yawSpeed MEMBER _yawSpeed) Q_PROPERTY(float pitchSpeed MEMBER _pitchSpeed) @@ -542,6 +545,9 @@ public: void setUseAdvancedMovementControls(bool useAdvancedMovementControls) { _useAdvancedMovementControls.set(useAdvancedMovementControls); } + bool getShowPlayArea() const { return _showPlayArea.get(); } + void setShowPlayArea(bool showPlayArea) { _showPlayArea.set(showPlayArea); } + void setHMDRollControlEnabled(bool value) { _hmdRollControlEnabled = value; } bool getHMDRollControlEnabled() const { return _hmdRollControlEnabled; } void setHMDRollControlDeadZone(float value) { _hmdRollControlDeadZone = value; } @@ -1631,6 +1637,7 @@ private: Setting::Handle _realWorldFieldOfView; Setting::Handle _useAdvancedMovementControls; + Setting::Handle _showPlayArea; // Smoothing. const float SMOOTH_TIME_ORIENTATION = 0.5f; diff --git a/interface/src/raypick/ParabolaPointer.cpp b/interface/src/raypick/ParabolaPointer.cpp index 33fa8738d9..9c33cb4637 100644 --- a/interface/src/raypick/ParabolaPointer.cpp +++ b/interface/src/raypick/ParabolaPointer.cpp @@ -18,6 +18,7 @@ const glm::vec4 ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR { 1.0f }; const float ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH { 0.01f }; const bool ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA { false }; +const bool ParabolaPointer::RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT { false }; gpu::PipelinePointer ParabolaPointer::RenderState::ParabolaRenderItem::_parabolaPipeline { nullptr }; gpu::PipelinePointer ParabolaPointer::RenderState::ParabolaRenderItem::_transparentParabolaPipeline { nullptr }; @@ -46,6 +47,7 @@ void ParabolaPointer::editRenderStatePath(const std::string& state, const QVaria float alpha = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR.a; float width = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH; bool isVisibleInSecondaryCamera = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA; + bool drawInFront = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT; bool enabled = false; if (!pathMap.isEmpty()) { enabled = true; @@ -63,8 +65,11 @@ void ParabolaPointer::editRenderStatePath(const std::string& state, const QVaria if (pathMap["isVisibleInSecondaryCamera"].isValid()) { isVisibleInSecondaryCamera = pathMap["isVisibleInSecondaryCamera"].toBool(); } + if (pathMap["drawInFront"].isValid()) { + drawInFront = pathMap["drawInFront"].toBool(); + } } - renderState->editParabola(color, alpha, width, isVisibleInSecondaryCamera, enabled); + renderState->editParabola(color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled); } } @@ -146,7 +151,7 @@ void ParabolaPointer::setVisualPickResultInternal(PickResultPointer pickResult, } ParabolaPointer::RenderState::RenderState(const OverlayID& startID, const OverlayID& endID, const glm::vec3& pathColor, float pathAlpha, float pathWidth, - bool isVisibleInSecondaryCamera, bool pathEnabled) : + bool isVisibleInSecondaryCamera, bool drawInFront, bool pathEnabled) : StartEndRenderState(startID, endID) { render::Transaction transaction; @@ -154,7 +159,7 @@ ParabolaPointer::RenderState::RenderState(const OverlayID& startID, const Overla _pathID = scene->allocateID(); _pathWidth = pathWidth; if (render::Item::isValidID(_pathID)) { - auto renderItem = std::make_shared(pathColor, pathAlpha, pathWidth, isVisibleInSecondaryCamera, pathEnabled); + auto renderItem = std::make_shared(pathColor, pathAlpha, pathWidth, isVisibleInSecondaryCamera, drawInFront, pathEnabled); transaction.resetItem(_pathID, std::make_shared(renderItem)); scene->enqueueTransaction(transaction); } @@ -182,15 +187,16 @@ void ParabolaPointer::RenderState::disable() { } } -void ParabolaPointer::RenderState::editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool enabled) { +void ParabolaPointer::RenderState::editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled) { if (render::Item::isValidID(_pathID)) { render::Transaction transaction; auto scene = qApp->getMain3DScene(); - transaction.updateItem(_pathID, [color, alpha, width, isVisibleInSecondaryCamera, enabled](ParabolaRenderItem& item) { + transaction.updateItem(_pathID, [color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled](ParabolaRenderItem& item) { item.setColor(color); item.setAlpha(alpha); item.setWidth(width); item.setIsVisibleInSecondaryCamera(isVisibleInSecondaryCamera); + item.setDrawInFront(drawInFront); item.setEnabled(enabled); item.updateKey(); }); @@ -238,6 +244,7 @@ std::shared_ptr ParabolaPointer::buildRenderState(const QVa float alpha = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_COLOR.a; float width = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_WIDTH; bool isVisibleInSecondaryCamera = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA; + bool drawInFront = RenderState::ParabolaRenderItem::DEFAULT_PARABOLA_DRAWINFRONT; bool enabled = false; if (propMap["path"].isValid()) { enabled = true; @@ -258,6 +265,10 @@ std::shared_ptr ParabolaPointer::buildRenderState(const QVa if (pathMap["isVisibleInSecondaryCamera"].isValid()) { isVisibleInSecondaryCamera = pathMap["isVisibleInSecondaryCamera"].toBool(); } + + if (pathMap["drawInFront"].isValid()) { + drawInFront = pathMap["drawInFront"].toBool(); + } } QUuid endID; @@ -269,7 +280,7 @@ std::shared_ptr ParabolaPointer::buildRenderState(const QVa } } - return std::make_shared(startID, endID, color, alpha, width, isVisibleInSecondaryCamera, enabled); + return std::make_shared(startID, endID, color, alpha, width, isVisibleInSecondaryCamera, drawInFront, enabled); } PointerEvent ParabolaPointer::buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button, bool hover) { @@ -321,8 +332,8 @@ glm::vec3 ParabolaPointer::findIntersection(const PickedObject& pickedObject, co } ParabolaPointer::RenderState::ParabolaRenderItem::ParabolaRenderItem(const glm::vec3& color, float alpha, float width, - bool isVisibleInSecondaryCamera, bool enabled) : - _isVisibleInSecondaryCamera(isVisibleInSecondaryCamera), _enabled(enabled) + bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled) : + _isVisibleInSecondaryCamera(isVisibleInSecondaryCamera), _drawInFront(drawInFront), _enabled(enabled) { _uniformBuffer->resize(sizeof(ParabolaData)); setColor(color); @@ -358,6 +369,10 @@ void ParabolaPointer::RenderState::ParabolaRenderItem::updateKey() { builder.withTagBits(render::hifi::TAG_MAIN_VIEW); } + if (_drawInFront) { + builder.withLayer(render::hifi::LAYER_3D_FRONT); + } + _key = builder.build(); } diff --git a/interface/src/raypick/ParabolaPointer.h b/interface/src/raypick/ParabolaPointer.h index 8fb864c07b..ae0bd086e2 100644 --- a/interface/src/raypick/ParabolaPointer.h +++ b/interface/src/raypick/ParabolaPointer.h @@ -21,7 +21,7 @@ public: using Pointer = Payload::DataPointer; ParabolaRenderItem(const glm::vec3& color, float alpha, float width, - bool isVisibleInSecondaryCamera, bool enabled); + bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled); ~ParabolaRenderItem() {} static gpu::PipelinePointer _parabolaPipeline; @@ -46,11 +46,13 @@ public: void setAcceleration(const glm::vec3& acceleration) { _parabolaData.acceleration = acceleration; } void setOrigin(const glm::vec3& origin) { _origin = origin; } void setIsVisibleInSecondaryCamera(const bool& isVisibleInSecondaryCamera) { _isVisibleInSecondaryCamera = isVisibleInSecondaryCamera; } + void setDrawInFront(const bool& drawInFront) { _drawInFront = drawInFront; } void setEnabled(const bool& enabled) { _enabled = enabled; } static const glm::vec4 DEFAULT_PARABOLA_COLOR; static const float DEFAULT_PARABOLA_WIDTH; static const bool DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA; + static const bool DEFAULT_PARABOLA_DRAWINFRONT; private: render::Item::Bound _bound; @@ -58,6 +60,7 @@ public: glm::vec3 _origin { 0.0f }; bool _isVisibleInSecondaryCamera { DEFAULT_PARABOLA_ISVISIBLEINSECONDARYCAMERA }; + bool _drawInFront { DEFAULT_PARABOLA_DRAWINFRONT }; bool _visible { false }; bool _enabled { false }; @@ -77,7 +80,7 @@ public: RenderState() {} RenderState(const OverlayID& startID, const OverlayID& endID, const glm::vec3& pathColor, float pathAlpha, float pathWidth, - bool isVisibleInSecondaryCamera, bool pathEnabled); + bool isVisibleInSecondaryCamera, bool drawInFront, bool pathEnabled); void setPathWidth(float width) { _pathWidth = width; } float getPathWidth() const { return _pathWidth; } @@ -87,7 +90,7 @@ public: void update(const glm::vec3& origin, const glm::vec3& end, const glm::vec3& surfaceNormal, bool scaleWithAvatar, bool distanceScaleEnd, bool centerEndY, bool faceAvatar, bool followNormal, float followNormalStrength, float distance, const PickResultPointer& pickResult) override; - void editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool enabled); + void editParabola(const glm::vec3& color, float alpha, float width, bool isVisibleInSecondaryCamera, bool drawInFront, bool enabled); private: int _pathID; diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index 7209e402a1..81ab023d20 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -218,6 +218,7 @@ unsigned int PointerScriptingInterface::createLaserPointer(const QVariant& prope * @property {number} alpha=1.0 The alpha of the parabola. * @property {number} width=0.01 The width of the parabola, in meters. * @property {boolean} isVisibleInSecondaryCamera=false The width of the parabola, in meters. +* @property {boolean} drawInFront=false If true, the parabola is rendered in front of other items in the scene. */ /**jsdoc * A set of properties used to define the visual aspect of a Parabola Pointer in the case that the Pointer is not intersecting something. Same as a {@link Pointers.ParabolaPointerRenderState}, @@ -393,4 +394,4 @@ QVariantMap PointerScriptingInterface::getPrevPickResult(unsigned int uid) const QVariantMap PointerScriptingInterface::getPointerProperties(unsigned int uid) const { return DependencyManager::get()->getPointerProperties(uid); -} \ No newline at end of file +} diff --git a/interface/src/raypick/PointerScriptingInterface.h b/interface/src/raypick/PointerScriptingInterface.h index 94f1d62552..2677f37fae 100644 --- a/interface/src/raypick/PointerScriptingInterface.h +++ b/interface/src/raypick/PointerScriptingInterface.h @@ -205,7 +205,7 @@ public: /**jsdoc * Returns information about an existing Pointer - * @function Pointers.getPointerState + * @function Pointers.getPointerProperties * @param {number} uid The ID of the Pointer, as returned by {@link Pointers.createPointer}. * @returns {Pointers.LaserPointerProperties|Pointers.StylusPointerProperties|Pointers.ParabolaPointerProperties} The information about the Pointer. * Currently only includes renderStates and defaultRenderStates with associated overlay IDs. diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index b648e125bf..4ba3813c4a 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -241,4 +241,4 @@ glm::vec2 StylusPointer::findPos2D(const PickedObject& pickedObject, const glm:: default: return glm::vec2(NAN); } -} \ No newline at end of file +} diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index ad8e265a01..ea24d6c793 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -201,3 +201,12 @@ bool HMDScriptingInterface::isKeyboardVisible() { void HMDScriptingInterface::centerUI() { QMetaObject::invokeMethod(qApp, "centerUI", Qt::QueuedConnection); } + +QVariant HMDScriptingInterface::getPlayAreaRect() { + auto rect = qApp->getActiveDisplayPlugin()->getPlayAreaRect(); + return qRectFToVariant(rect); +} + +QVector HMDScriptingInterface::getSensorPositions() { + return qApp->getActiveDisplayPlugin()->getSensorPositions(); +} diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 78744b320b..2c0a3fe45f 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -61,6 +61,8 @@ class QScriptEngine; * @property {Uuid} miniTabletScreenID - The UUID of the mini tablet's screen overlay. null if not in HMD mode. * @property {number} miniTabletHand - The hand that the mini tablet is displayed on: 0 for left hand, * 1 for right hand, -1 if not in HMD mode. + * @property {Rect} playArea=0,0,0,0 - The size and position of the HMD play area in sensor coordinates. Read-only. + * @property {Vec3[]} sensorPositions=[]] - The positions of the VR system sensors in sensor coordinates. Read-only. */ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Dependency { Q_OBJECT @@ -75,6 +77,8 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_PROPERTY(QUuid miniTabletID READ getCurrentMiniTabletID WRITE setCurrentMiniTabletID) Q_PROPERTY(QUuid miniTabletScreenID READ getCurrentMiniTabletScreenID WRITE setCurrentMiniTabletScreenID) Q_PROPERTY(int miniTabletHand READ getCurrentMiniTabletHand WRITE setCurrentMiniTabletHand) + Q_PROPERTY(QVariant playArea READ getPlayAreaRect); + Q_PROPERTY(QVector sensorPositions READ getSensorPositions); public: @@ -384,6 +388,9 @@ public: void setCurrentMiniTabletHand(int miniTabletHand) { _miniTabletHand = miniTabletHand; } int getCurrentMiniTabletHand() const { return _miniTabletHand; } + QVariant getPlayAreaRect(); + QVector getSensorPositions(); + private: bool _showTablet { false }; bool _tabletContextualMode { false }; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 951925214c..5eccef5e9d 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -226,18 +226,22 @@ void setupPreferences() { static const QString VR_MOVEMENT{ "VR Movement" }; { - - static const QString movementsControlChannel = QStringLiteral("Hifi-Advanced-Movement-Disabler"); - auto getter = [myAvatar]()->bool { return myAvatar->useAdvancedMovementControls(); }; - auto setter = [myAvatar](bool value) { myAvatar->setUseAdvancedMovementControls(value); }; - preferences->addPreference(new CheckPreference(VR_MOVEMENT, - QStringLiteral("Advanced movement in VR (Teleport movement when unchecked)"), - getter, setter)); + auto getter = [myAvatar]()->int { return myAvatar->useAdvancedMovementControls() ? 1 : 0; }; + auto setter = [myAvatar](int value) { myAvatar->setUseAdvancedMovementControls(value == 1); }; + auto preference = + new RadioButtonsPreference(VR_MOVEMENT, "Teleporting only / Walking and teleporting", getter, setter); + QStringList items; + items << "Teleporting only" << "Walking and teleporting"; + preference->setHeading("Movement mode"); + preference->setItems(items); + preferences->addPreference(preference); } { auto getter = [myAvatar]()->bool { return myAvatar->getFlyingHMDPref(); }; auto setter = [myAvatar](bool value) { myAvatar->setFlyingHMDPref(value); }; - preferences->addPreference(new CheckPreference(VR_MOVEMENT, "Flying & jumping (HMD)", getter, setter)); + auto preference = new CheckPreference(VR_MOVEMENT, "Jumping and flying", getter, setter); + preference->setIndented(true); + preferences->addPreference(preference); } { auto getter = [myAvatar]()->int { return myAvatar->getSnapTurn() ? 0 : 1; }; @@ -245,9 +249,16 @@ void setupPreferences() { auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Snap turn / Smooth turn", getter, setter); QStringList items; items << "Snap turn" << "Smooth turn"; + preference->setHeading("Rotation mode"); preference->setItems(items); preferences->addPreference(preference); } + { + auto getter = [myAvatar]()->bool { return myAvatar->getShowPlayArea(); }; + auto setter = [myAvatar](bool value) { myAvatar->setShowPlayArea(value); }; + auto preference = new CheckPreference(VR_MOVEMENT, "Show room boundaries while teleporting", getter, setter); + preferences->addPreference(preference); + } { auto getter = [=]()->float { return myAvatar->getUserHeight(); }; auto setter = [=](float value) { myAvatar->setUserHeight(value); }; @@ -258,12 +269,6 @@ void setupPreferences() { preference->setStep(0.001f); preferences->addPreference(preference); } - { - auto preference = new ButtonPreference(VR_MOVEMENT, "RESET SENSORS", [] { - qApp->resetSensors(); - }); - preferences->addPreference(preference); - } static const QString AVATAR_CAMERA{ "Mouse Sensitivity" }; { diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index f49b41cbe6..ce730bffa8 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -178,6 +178,17 @@ namespace controller { return inputRecorder->getSaveDirectory(); } + QStringList ScriptingInterface::getRunningInputDeviceNames() { + QMutexLocker locker(&_runningDevicesMutex); + return _runningInputDeviceNames; + } + + void ScriptingInterface::updateRunningInputDevices(const QString& deviceName, bool isRunning, const QStringList& runningDevices) { + QMutexLocker locker(&_runningDevicesMutex); + _runningInputDeviceNames = runningDevices; + emit inputDeviceRunningChanged(deviceName, isRunning); + } + bool ScriptingInterface::triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand) const { return DependencyManager::get()->triggerHapticPulseOnDevice(device, strength, duration, hand); } diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index b0004bc12d..157730e7c6 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -431,6 +432,13 @@ namespace controller { */ Q_INVOKABLE QString getInputRecorderSaveDirectory(); + /**jsdoc + * Get all the active and enabled (running) input devices + * @function Controller.getRunningInputDevices + * @returns {string[]} An array of strings with the names + */ + Q_INVOKABLE QStringList getRunningInputDeviceNames(); + bool isMouseCaptured() const { return _mouseCaptured; } bool isTouchCaptured() const { return _touchCaptured; } bool isWheelCaptured() const { return _wheelCaptured; } @@ -531,6 +539,8 @@ namespace controller { */ virtual void releaseActionEvents() { _actionsCaptured = false; } + void updateRunningInputDevices(const QString& deviceName, bool isRunning, const QStringList& runningDevices); + signals: /**jsdoc * Triggered when an action occurs. @@ -590,6 +600,17 @@ namespace controller { */ void hardwareChanged(); + /**jsdoc + * Triggered when a device is enabled/disabled + * Enabling/Disabling Leapmotion on settings/controls will trigger this signal. + * @function Controller.deviceRunningChanged + * @param {string} deviceName - The name of the device that is getting enabled/disabled + * @param {boolean} isEnabled - Return if the device is enabled. + * @returns {Signal} + */ + void inputDeviceRunningChanged(QString deviceName, bool isRunning); + + private: // Update the exposed variant maps reporting active hardware void updateMaps(); @@ -598,10 +619,14 @@ namespace controller { QVariantMap _actions; QVariantMap _standard; + QStringList _runningInputDeviceNames; + std::atomic _mouseCaptured{ false }; std::atomic _touchCaptured { false }; std::atomic _wheelCaptured { false }; std::atomic _actionsCaptured { false }; + + QMutex _runningDevicesMutex; }; } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index e330f6d7ec..ad49ceafe6 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -112,6 +112,9 @@ public: virtual bool suppressKeyboard() { return false; } virtual void unsuppressKeyboard() {}; virtual bool isKeyboardVisible() { return false; } + + virtual QRectF getPlayAreaRect() { return QRectF(); } + virtual QVector getSensorPositions() { return QVector(); } }; class DisplayPlugin : public Plugin, public HmdDisplay { diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index 2d4a24a1fe..a0494ba6d8 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -75,8 +75,11 @@ public: virtual void saveSettings() const {} virtual void loadSettings() {} + virtual bool isRunning() const { return _active; } signals: + void deviceStatusChanged(const QString& deviceName, bool isRunning) const; + // These signals should be emitted when a device is first known to be available. In some cases this will // be in `init()`, in other cases, like Neuron, this isn't known until activation. // SDL2 isn't a device itself, but can have 0+ subdevices. subdeviceConnected is used in this case. @@ -85,6 +88,7 @@ signals: protected: bool _active { false }; + bool _enabled { false }; bool _sessionStatus { false }; PluginContainer* _container { nullptr }; static const char* UNKNOWN_PLUGIN_ID; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 94ce16cf00..32d8486e7a 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -225,8 +225,11 @@ void PluginManager::disableDisplayPlugin(const QString& name) { const InputPluginList& PluginManager::getInputPlugins() { static std::once_flag once; - static auto deviceAddedCallback = [](QString deviceName) { + static auto deviceAddedCallback = [&](QString deviceName) { qCDebug(plugins) << "Added device: " << deviceName; + QStringList runningDevices = getRunningInputDeviceNames(); + bool isDeviceRunning = runningDevices.indexOf(deviceName) >= 0; + emit inputDeviceRunningChanged(deviceName, isDeviceRunning, runningDevices); UserActivityLogger::getInstance().connectedDevice("input", deviceName); }; static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { @@ -252,6 +255,9 @@ const InputPluginList& PluginManager::getInputPlugins() { for (auto plugin : _inputPlugins) { connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::deviceStatusChanged, this, [&](const QString& deviceName, bool isRunning) { + emit inputDeviceRunningChanged(deviceName, isRunning, getRunningInputDeviceNames()); + }, Qt::QueuedConnection); plugin->setContainer(_container); plugin->init(); } @@ -259,6 +265,16 @@ const InputPluginList& PluginManager::getInputPlugins() { return _inputPlugins; } +QStringList PluginManager::getRunningInputDeviceNames() const { + QStringList runningDevices; + for (auto plugin: _inputPlugins) { + if (plugin->isRunning()) { + runningDevices << plugin->getName(); + } + } + return runningDevices; +} + void PluginManager::setPreferredDisplayPlugins(const QStringList& displays) { preferredDisplayPlugins = displays; } diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 65a4012aed..c7489fd7e4 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -19,6 +19,7 @@ using PluginManagerPointer = QSharedPointer; class PluginManager : public QObject, public Dependency { SINGLETON_DEPENDENCY + Q_OBJECT public: static PluginManagerPointer getInstance(); @@ -44,6 +45,10 @@ public: void setInputPluginProvider(const InputPluginProvider& provider); void setCodecPluginProvider(const CodecPluginProvider& provider); void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister); + QStringList getRunningInputDeviceNames() const; + +signals: + void inputDeviceRunningChanged(const QString& pluginName, bool isRunning, const QStringList& runningDevices); private: PluginManager() = default; diff --git a/libraries/pointers/src/PointerManager.cpp b/libraries/pointers/src/PointerManager.cpp index 922f0bb5bc..72e4b417cd 100644 --- a/libraries/pointers/src/PointerManager.cpp +++ b/libraries/pointers/src/PointerManager.cpp @@ -153,4 +153,4 @@ bool PointerManager::isMouse(unsigned int uid) { return pointer->isMouse(); } return false; -} \ No newline at end of file +} diff --git a/libraries/render-utils/src/Highlight.slh b/libraries/render-utils/src/Highlight.slh index b26337676f..885df34d26 100644 --- a/libraries/render-utils/src/Highlight.slh +++ b/libraries/render-utils/src/Highlight.slh @@ -26,7 +26,6 @@ layout(location=0) in vec2 varTexCoord0; layout(location=0) out vec4 outFragColor; const float FAR_Z = 1.0; -const float LINEAR_DEPTH_BIAS = 5e-3; const float OPACITY_EPSILON = 5e-3; <@func main(IS_FILLED)@> @@ -46,7 +45,7 @@ void main(void) { highlightedDepth = -evalZeyeFromZdb(highlightedDepth); sceneDepth = -evalZeyeFromZdb(sceneDepth); - if (sceneDepth < (highlightedDepth-LINEAR_DEPTH_BIAS)) { + if (sceneDepth < highlightedDepth) { outFragColor = vec4(params._fillOccludedColor, params._fillOccludedAlpha); } else { outFragColor = vec4(params._fillUnoccludedColor, params._fillUnoccludedAlpha); @@ -107,7 +106,7 @@ void main(void) { sceneDepth = -evalZeyeFromZdb(sceneDepth); // Are we occluded? - if (sceneDepth < (outlinedDepth/*-LINEAR_DEPTH_BIAS*/)) { + if (sceneDepth < outlinedDepth) { outFragColor = vec4(params._outlineOccludedColor, intensity * params._outlineOccludedAlpha); } else { outFragColor = vec4(params._outlineUnoccludedColor, intensity * params._outlineUnoccludedAlpha); diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index 27bcf7a71b..4a58d71c54 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -340,10 +340,16 @@ public: class CheckPreference : public BoolPreference { Q_OBJECT + Q_PROPERTY(bool indented READ getIndented CONSTANT) public: CheckPreference(const QString& category, const QString& name, Getter getter, Setter setter) : BoolPreference(category, name, getter, setter) { } Type getType() override { return Checkbox; } + + bool getIndented() { return _isIndented; } + void setIndented(const bool indented) { _isIndented = indented; } +protected: + bool _isIndented { false }; }; class PrimaryHandPreference : public StringPreference { @@ -356,16 +362,20 @@ public: class RadioButtonsPreference : public IntPreference { Q_OBJECT + Q_PROPERTY(QString heading READ getHeading CONSTANT) Q_PROPERTY(QStringList items READ getItems CONSTANT) public: RadioButtonsPreference(const QString& category, const QString& name, Getter getter, Setter setter) : IntPreference(category, name, getter, setter) { } Type getType() override { return RadioButtons; } + const QString& getHeading() { return _heading; } const QStringList& getItems() { return _items; } + void setHeading(const QString& heading) { _heading = heading; } void setItems(const QStringList& items) { _items = items; } protected: + QString _heading; QStringList _items; }; #endif diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index a9dbe83b06..0c8f4f0466 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -552,6 +552,14 @@ glm::vec2 vec2FromVariant(const QVariant &object) { return vec2FromVariant(object, valid); } +/**jsdoc + * Defines a rectangular portion of an image or screen, or similar. + * @typedef {object} Rect + * @property {number} x - Left, x-coordinate value. + * @property {number} y - Top, y-coordinate value. + * @property {number} width - Width of the rectangle. + * @property {number} height - Height of the rectangle. + */ QScriptValue qRectToScriptValue(QScriptEngine* engine, const QRect& rect) { QScriptValue obj = engine->newObject(); obj.setProperty("x", rect.x()); @@ -568,22 +576,6 @@ void qRectFromScriptValue(const QScriptValue &object, QRect& rect) { rect.setHeight(object.property("height").toVariant().toInt()); } -QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) { - QScriptValue obj = engine->newObject(); - obj.setProperty("red", color.red); - obj.setProperty("green", color.green); - obj.setProperty("blue", color.blue); - return obj; -} - -/**jsdoc - * Defines a rectangular portion of an image or screen. - * @typedef {object} Rect - * @property {number} x - Integer left, x-coordinate value. - * @property {number} y - Integer top, y-coordinate value. - * @property {number} width - Integer width of the rectangle. - * @property {number} height - Integer height of the rectangle. - */ QVariant qRectToVariant(const QRect& rect) { QVariantMap obj; obj["x"] = rect.x(); @@ -615,6 +607,61 @@ QRect qRectFromVariant(const QVariant& object) { return qRectFromVariant(object, valid); } +QScriptValue qRectFToScriptValue(QScriptEngine* engine, const QRectF& rect) { + QScriptValue obj = engine->newObject(); + obj.setProperty("x", rect.x()); + obj.setProperty("y", rect.y()); + obj.setProperty("width", rect.width()); + obj.setProperty("height", rect.height()); + return obj; +} + +void qRectFFromScriptValue(const QScriptValue &object, QRectF& rect) { + rect.setX(object.property("x").toVariant().toFloat()); + rect.setY(object.property("y").toVariant().toFloat()); + rect.setWidth(object.property("width").toVariant().toFloat()); + rect.setHeight(object.property("height").toVariant().toFloat()); +} + +QVariant qRectFToVariant(const QRectF& rect) { + QVariantMap obj; + obj["x"] = rect.x(); + obj["y"] = rect.y(); + obj["width"] = rect.width(); + obj["height"] = rect.height(); + return obj; +} + +QRectF qRectFFromVariant(const QVariant& objectVar, bool& valid) { + QVariantMap object = objectVar.toMap(); + QRectF rect; + valid = false; + rect.setX(object["x"].toFloat(&valid)); + if (valid) { + rect.setY(object["y"].toFloat(&valid)); + } + if (valid) { + rect.setWidth(object["width"].toFloat(&valid)); + } + if (valid) { + rect.setHeight(object["height"].toFloat(&valid)); + } + return rect; +} + +QRectF qRectFFromVariant(const QVariant& object) { + bool valid; + return qRectFFromVariant(object, valid); +} + + +QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) { + QScriptValue obj = engine->newObject(); + obj.setProperty("red", color.red); + obj.setProperty("green", color.green); + obj.setProperty("blue", color.blue); + return obj; +} void xColorFromScriptValue(const QScriptValue &object, xColor& color) { if (!object.isValid()) { diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 6ecf9faca7..ea576b6e33 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -100,7 +100,11 @@ void qRectFromScriptValue(const QScriptValue& object, QRect& rect); QRect qRectFromVariant(const QVariant& object, bool& isValid); QRect qRectFromVariant(const QVariant& object); QVariant qRectToVariant(const QRect& rect); - +QScriptValue qRectFToScriptValue(QScriptEngine* engine, const QRectF& rect); +void qRectFFromScriptValue(const QScriptValue& object, QRectF& rect); +QRectF qRectFFromVariant(const QVariant& object, bool& isValid); +QRectF qRectFFromVariant(const QVariant& object); +QVariant qRectFToVariant(const QRectF& rect); // xColor QScriptValue xColorToScriptValue(QScriptEngine* engine, const xColor& color); diff --git a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp index 174dd02426..5c5b975676 100644 --- a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp +++ b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp @@ -21,7 +21,7 @@ Q_DECLARE_LOGGING_CATEGORY(inputplugins) Q_LOGGING_CATEGORY(inputplugins, "hifi.inputplugins") -const char* LeapMotionPlugin::NAME = "Leap Motion"; +const char* LeapMotionPlugin::NAME = "LeapMotion"; const char* LeapMotionPlugin::LEAPMOTION_ID_STRING = "Leap Motion"; const bool DEFAULT_ENABLED = false; @@ -203,7 +203,6 @@ static const char* getControllerJointName(controller::StandardPoseChannel i) { return "unknown"; } - void LeapMotionPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (!_enabled) { return; @@ -312,13 +311,13 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In void LeapMotionPlugin::init() { loadSettings(); - auto preferences = DependencyManager::get(); static const QString LEAPMOTION_PLUGIN { "Leap Motion" }; { auto getter = [this]()->bool { return _enabled; }; auto setter = [this](bool value) { _enabled = value; + emit deviceStatusChanged(getName(), isRunning()); saveSettings(); if (!_enabled) { auto userInputMapper = DependencyManager::get(); @@ -406,6 +405,7 @@ void LeapMotionPlugin::loadSettings() { settings.beginGroup(idString); { _enabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool(); + emit deviceStatusChanged(getName(), isRunning()); _sensorLocation = settings.value(SETTINGS_SENSOR_LOCATION_KEY, QVariant(DEFAULT_SENSOR_LOCATION)).toString(); _desktopHeightOffset = settings.value(SETTINGS_DESKTOP_HEIGHT_OFFSET_KEY, QVariant(DEFAULT_DESKTOP_HEIGHT_OFFSET)).toFloat(); diff --git a/plugins/hifiLeapMotion/src/LeapMotionPlugin.h b/plugins/hifiLeapMotion/src/LeapMotionPlugin.h index 391d569567..7f5a81d58a 100644 --- a/plugins/hifiLeapMotion/src/LeapMotionPlugin.h +++ b/plugins/hifiLeapMotion/src/LeapMotionPlugin.h @@ -30,7 +30,7 @@ public: // Plugin methods virtual const QString getName() const override { return NAME; } const QString getID() const override { return LEAPMOTION_ID_STRING; } - + bool isRunning() const override { return _active && _enabled; } virtual void init() override; virtual bool activate() override; @@ -43,8 +43,6 @@ protected: static const char* NAME; static const char* LEAPMOTION_ID_STRING; const float DEFAULT_DESKTOP_HEIGHT_OFFSET = 0.2f; - - bool _enabled { false }; QString _sensorLocation; float _desktopHeightOffset { DEFAULT_DESKTOP_HEIGHT_OFFSET }; diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 4e27777628..15e99932e1 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -369,7 +369,11 @@ void NeuronPlugin::init() { static const QString NEURON_PLUGIN { "Perception Neuron" }; { auto getter = [this]()->bool { return _enabled; }; - auto setter = [this](bool value) { _enabled = value; saveSettings(); }; + auto setter = [this](bool value) { + _enabled = value; + saveSettings(); + emit deviceStatusChanged(getName(), _enabled && _active); + }; auto preference = new CheckPreference(NEURON_PLUGIN, "Enabled", getter, setter); preferences->addPreference(preference); } @@ -493,7 +497,7 @@ void NeuronPlugin::loadSettings() { { // enabled _enabled = settings.value("enabled", QVariant(DEFAULT_ENABLED)).toBool(); - + emit deviceStatusChanged(getName(), _enabled && _active); // serverAddress _serverAddress = settings.value("serverAddress", QVariant(DEFAULT_SERVER_ADDRESS)).toString(); diff --git a/plugins/hifiNeuron/src/NeuronPlugin.h b/plugins/hifiNeuron/src/NeuronPlugin.h index 34d084160f..c2909c54fb 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.h +++ b/plugins/hifiNeuron/src/NeuronPlugin.h @@ -30,7 +30,7 @@ public: virtual bool isSupported() const override; virtual const QString getName() const override { return NAME; } const QString getID() const override { return NEURON_ID_STRING; } - + bool isRunning() const override { return _active && _enabled; } virtual bool activate() override; virtual void deactivate() override; @@ -67,7 +67,6 @@ protected: static const char* NAME; static const char* NEURON_ID_STRING; - bool _enabled; QString _serverAddress; int _serverPort; void* _socketRef; diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index df0cef06c8..7b042909a8 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -79,10 +79,11 @@ bool SDL2Manager::activate() { auto preferences = DependencyManager::get(); static const QString SDL2_PLUGIN { "Game Controller" }; { - auto getter = [this]()->bool { return _isEnabled; }; + auto getter = [this]()->bool { return _enabled; }; auto setter = [this](bool value) { - _isEnabled = value; + _enabled = value; saveSettings(); + emit deviceStatusChanged(getName(), isRunning()); }; auto preference = new CheckPreference(SDL2_PLUGIN, "Enabled", getter, setter); preferences->addPreference(preference); @@ -147,7 +148,7 @@ void SDL2Manager::saveSettings() const { QString idString = getID(); settings.beginGroup(idString); { - settings.setValue(QString(SETTINGS_ENABLED_KEY), _isEnabled); + settings.setValue(QString(SETTINGS_ENABLED_KEY), _enabled); } settings.endGroup(); } @@ -157,7 +158,8 @@ void SDL2Manager::loadSettings() { QString idString = getID(); settings.beginGroup(idString); { - _isEnabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool(); + _enabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool(); + emit deviceStatusChanged(getName(), isRunning()); } settings.endGroup(); } @@ -173,7 +175,7 @@ void SDL2Manager::pluginFocusOutEvent() { } void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { - if (!_isEnabled) { + if (!_enabled) { return; } diff --git a/plugins/hifiSdl2/src/SDL2Manager.h b/plugins/hifiSdl2/src/SDL2Manager.h index 48e779a204..0daad6fd8b 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.h +++ b/plugins/hifiSdl2/src/SDL2Manager.h @@ -26,7 +26,7 @@ public: bool isSupported() const override; const QString getName() const override { return NAME; } const QString getID() const override { return SDL2_ID_STRING; } - + bool isRunning() const override { return _active && _enabled; } QStringList getSubdeviceNames() override; void init() override; @@ -81,7 +81,6 @@ private: int buttonRelease() const { return SDL_RELEASED; } QMap _openJoysticks; - bool _isEnabled { false }; bool _isInitialized { false }; static const char* NAME; static const char* SDL2_ID_STRING; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 36790d1b50..0e4dac796d 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -171,3 +171,53 @@ void OculusBaseDisplayPlugin::updatePresentPose() { _currentPresentFrameInfo.presentPose = ovr::toGlm(trackingState.HeadPose.ThePose); _currentPresentFrameInfo.renderPose = _currentPresentFrameInfo.presentPose; } + +QRectF OculusBaseDisplayPlugin::getPlayAreaRect() { + if (!_session) { + return QRectF(); + } + + int floorPointsCount = 0; + auto result = ovr_GetBoundaryGeometry(_session, ovrBoundary_PlayArea, nullptr, &floorPointsCount); + if (!OVR_SUCCESS(result) || floorPointsCount != 4) { + return QRectF(); + } + + auto floorPoints = new ovrVector3f[floorPointsCount]; + result = ovr_GetBoundaryGeometry(_session, ovrBoundary_PlayArea, floorPoints, nullptr); + if (!OVR_SUCCESS(result)) { + return QRectF(); + } + + auto minXZ = ovr::toGlm(floorPoints[0]); + auto maxXZ = minXZ; + for (int i = 1; i < floorPointsCount; i++) { + auto point = ovr::toGlm(floorPoints[i]); + minXZ.x = std::min(minXZ.x, point.x); + minXZ.z = std::min(minXZ.z, point.z); + maxXZ.x = std::max(maxXZ.x, point.x); + maxXZ.z = std::max(maxXZ.z, point.z); + } + + glm::vec2 center = glm::vec2((minXZ.x + maxXZ.x) / 2, (minXZ.z + maxXZ.z) / 2); + glm::vec2 dimensions = glm::vec2(maxXZ.x - minXZ.x, maxXZ.z - minXZ.z); + + return QRectF(center.x, center.y, dimensions.x, dimensions.y); +} + +QVector OculusBaseDisplayPlugin::getSensorPositions() { + if (!_session) { + return QVector(); + } + + QVector result; + auto numTrackers = ovr_GetTrackerCount(_session); + for (uint i = 0; i < numTrackers; i++) { + auto trackerPose = ovr_GetTrackerPose(_session, i); + if (trackerPose.TrackerFlags & ovrTracker_PoseTracked) { + result.append(ovr::toGlm(trackerPose.Pose.Position)); + } + } + + return result; +} diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 244c06ecf5..f71fb8ece8 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -27,6 +27,9 @@ public: void resetSensors() override final; bool beginFrameRender(uint32_t frameIndex) override; float getTargetFrameRate() const override { return _hmdDesc.DisplayRefreshRate; } + + QRectF getPlayAreaRect() override; + QVector getSensorPositions() override; protected: void customizeContext() override; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index fae2144caf..ef0ac65c2a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -750,3 +750,37 @@ QString OpenVrDisplayPlugin::getPreferredAudioOutDevice() const { } return device; } + +QRectF OpenVrDisplayPlugin::getPlayAreaRect() { + auto chaperone = vr::VRChaperone(); + if (!chaperone) { + qWarning() << "No chaperone"; + return QRectF(); + } + + if (chaperone->GetCalibrationState() >= vr::ChaperoneCalibrationState_Error) { + qWarning() << "Chaperone status =" << chaperone->GetCalibrationState(); + return QRectF(); + } + + vr::HmdQuad_t rect; + if (!chaperone->GetPlayAreaRect(&rect)) { + qWarning() << "Chaperone rect not obtained"; + return QRectF(); + } + + auto minXZ = transformPoint(_sensorResetMat, toGlm(rect.vCorners[0])); + auto maxXZ = minXZ; + for (int i = 1; i < 4; i++) { + auto point = transformPoint(_sensorResetMat, toGlm(rect.vCorners[i])); + minXZ.x = std::min(minXZ.x, point.x); + minXZ.z = std::min(minXZ.z, point.z); + maxXZ.x = std::max(maxXZ.x, point.x); + maxXZ.z = std::max(maxXZ.z, point.z); + } + + glm::vec2 center = glm::vec2((minXZ.x + maxXZ.x) / 2, (minXZ.z + maxXZ.z) / 2); + glm::vec2 dimensions = glm::vec2(maxXZ.x - minXZ.x, maxXZ.z - minXZ.z); + + return QRectF(center.x, center.y, dimensions.x, dimensions.y); +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index add35d6383..5585957031 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -64,6 +64,8 @@ public: QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; + QRectF getPlayAreaRect() override; + protected: bool internalActivate() override; void internalDeactivate() override; diff --git a/scripts/system/assets/models/oculusSensorv11.fbx b/scripts/system/assets/models/oculusSensorv11.fbx new file mode 100644 index 0000000000..52fadc77dc Binary files /dev/null and b/scripts/system/assets/models/oculusSensorv11.fbx differ diff --git a/scripts/system/assets/models/teleportationSpotBasev8.fbx b/scripts/system/assets/models/teleportationSpotBasev8.fbx new file mode 100644 index 0000000000..d651575dea Binary files /dev/null and b/scripts/system/assets/models/teleportationSpotBasev8.fbx differ diff --git a/scripts/system/assets/models/trackingSpacev18.fbx b/scripts/system/assets/models/trackingSpacev18.fbx new file mode 100644 index 0000000000..16597eb285 Binary files /dev/null and b/scripts/system/assets/models/trackingSpacev18.fbx differ diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index deaa934f99..2381c2fe46 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -21,14 +21,10 @@ Script.include("/~/system/libraries/controllers.js"); (function() { // BEGIN LOCAL_SCOPE - var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleport-destination.fbx"); + var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleportationSpotBasev8.fbx"); var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); - var TARGET_MODEL_DIMENSIONS = { - x: 1.15, - y: 0.5, - z: 1.15 - }; + var TARGET_MODEL_DIMENSIONS = { x: 0.6552, y: 0.3063, z: 0.6552 }; var COLORS_TELEPORT_SEAT = { red: 255, @@ -59,20 +55,23 @@ Script.include("/~/system/libraries/controllers.js"); var cancelPath = { color: COLORS_TELEPORT_CANCEL, - alpha: 1, - width: 0.025 + alpha: 0.3, + width: 0.025, + drawInFront: true }; var teleportPath = { color: COLORS_TELEPORT_CAN_TELEPORT, - alpha: 1, - width: 0.025 + alpha: 0.7, + width: 0.025, + drawInFront: true }; var seatPath = { color: COLORS_TELEPORT_SEAT, - alpha: 1, - width: 0.025 + alpha: 0.7, + width: 0.025, + drawInFront: true }; var teleportEnd = { @@ -150,19 +149,149 @@ Script.include("/~/system/libraries/controllers.js"); this.teleportParabolaHeadVisuals; this.teleportParabolaHeadCollisions; + + this.PLAY_AREA_OVERLAY_MODEL = Script.resolvePath("../../assets/models/trackingSpacev18.fbx"); + this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS = { x: 1.969, y: 0.001, z: 1.969 }; + this.PLAY_AREA_FLOAT_ABOVE_FLOOR = 0.005; + this.PLAY_AREA_OVERLAY_OFFSET = // Offset from floor. + { x: 0, y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2 + this.PLAY_AREA_FLOAT_ABOVE_FLOOR, z: 0 }; + this.PLAY_AREA_SENSOR_OVERLAY_MODEL = Script.resolvePath("../../assets/models/oculusSensorv11.fbx"); + this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS = { x: 0.1198, y: 0.2981, z: 0.1198 }; + this.PLAY_AREA_SENSOR_OVERLAY_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }); + this.PLAY_AREA_BOX_ALPHA = 1.0; + this.PLAY_AREA_SENSOR_ALPHA = 0.8; + this.playAreaSensorPositions = []; + this.playArea = { x: 0, y: 0 }; + this.playAreaCenterOffset = this.PLAY_AREA_OVERLAY_OFFSET; + this.isPlayAreaVisible = false; + this.wasPlayAreaVisible = false; + this.isPlayAreaAvailable = false; + this.targetOverlayID = null; + this.playAreaOverlay = null; + this.playAreaSensorPositionOverlays = []; + + this.TELEPORT_SCALE_DURATION = 130; + this.TELEPORT_SCALE_TIMEOUT = 25; + this.isTeleportVisible = false; + this.teleportScaleTimer = null; + this.teleportScaleStart = 0; + this.teleportScaleFactor = 0; + this.teleportScaleMode = "head"; + + this.TELEPORTED_FADE_DELAY_DURATION = 900; + this.TELEPORTED_FADE_DURATION = 200; + this.TELEPORTED_FADE_INTERVAL = 25; + this.TELEPORTED_FADE_DELAY_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DELAY_DURATION; + this.TELEPORTED_FADE_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DURATION; + this.teleportedFadeTimer = null; + this.teleportedFadeDelayFactor = 0; + this.teleportedFadeFactor = 0; + this.teleportedPosition = Vec3.ZERO; + this.TELEPORTED_TARGET_ALPHA = 1.0; + this.TELEPORTED_TARGET_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }); + this.teleportedTargetOverlay = null; + + this.setPlayAreaDimensions = function () { + var avatarScale = MyAvatar.sensorToWorldScale; + + var playAreaOverlayProperties = { + dimensions: + Vec3.multiply(this.teleportScaleFactor * avatarScale, { + x: this.playArea.width, + y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y, + z: this.playArea.height + }) + }; + + if (this.teleportScaleFactor < 1) { + // Adjust position of playAreOverlay so that its base is at correct height. + // Always parenting to teleport target is good enough for this. + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation"); + var relativePlayAreaCenterOffset = + Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + var localPosition = Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(avatarScale, Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))); + localPosition.y = this.teleportScaleFactor * localPosition.y; + + playAreaOverlayProperties.parentID = this.targetOverlayID; + playAreaOverlayProperties.localPosition = localPosition; + } + + Overlays.editOverlay(this.playAreaOverlay, playAreaOverlayProperties); + + for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) { + localPosition = this.playAreaSensorPositions[i]; + localPosition = Vec3.multiply(avatarScale, localPosition); + // Position relative to the play area. + localPosition.y = avatarScale * (this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2 + - this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2); + Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.multiply(this.teleportScaleFactor * avatarScale, this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS), + parentID: this.playAreaOverlay, + localPosition: localPosition + }); + } + }; + + this.updatePlayAreaScale = function () { + if (this.isPlayAreaAvailable) { + this.setPlayAreaDimensions(); + } + }; + + + this.teleporterSelectionName = "teleporterSelection" + hand.toString(); + this.TELEPORTER_SELECTION_STYLE = { + outlineUnoccludedColor: { red: 0, green: 0, blue: 0 }, + outlineUnoccludedAlpha: 0, + outlineOccludedColor: { red: 0, green: 0, blue: 0 }, + outlineOccludedAlpha: 0, + fillUnoccludedColor: { red: 0, green: 0, blue: 0 }, + fillUnoccludedAlpha: 0, + fillOccludedColor: { red: 0, green: 0, blue: 255 }, + fillOccludedAlpha: 0.84, + outlineWidth: 0, + isOutlineSmooth: false + }; + + this.addToSelectedItemsList = function (properties) { + for (var i = 0, length = teleportRenderStates.length; i < length; i++) { + var state = properties.renderStates[teleportRenderStates[i].name]; + if (state && state.end) { + Selection.addToSelectedItemsList(this.teleporterSelectionName, "overlay", state.end); + } + } + }; + + this.cleanup = function() { + Selection.removeListFromMap(_this.teleporterSelectionName); Pointers.removePointer(_this.teleportParabolaHandVisuals); Pointers.removePointer(_this.teleportParabolaHandCollisions); Pointers.removePointer(_this.teleportParabolaHeadVisuals); Pointers.removePointer(_this.teleportParabolaHeadCollisions); Picks.removePick(_this.teleportHandCollisionPick); Picks.removePick(_this.teleportHeadCollisionPick); + Overlays.deleteOverlay(_this.teleportedTargetOverlay); + Overlays.deleteOverlay(_this.playAreaOverlay); + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + Overlays.deleteOverlay(_this.playAreaSensorPositionOverlays[i]); + } + _this.playAreaSensorPositionOverlays = []; }; - this.initPointers = function () { + this.initPointers = function() { if (_this.init) { _this.cleanup(); } + _this.teleportParabolaHandVisuals = Pointers.createPointer(PickType.Parabola, { joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", dirOffset: { x: 0, y: 1, z: 0.1 }, @@ -221,6 +350,9 @@ Script.include("/~/system/libraries/controllers.js"); maxDistance: 8.0 }); + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHandVisuals)); + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHeadVisuals)); + var capsuleData = MyAvatar.getCollisionCapsule(); @@ -262,11 +394,264 @@ Script.include("/~/system/libraries/controllers.js"); position: { x: 0, y: offset + height * 0.5, z: 0 }, threshold: _this.capsuleThreshold }); + + + _this.playAreaOverlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_OVERLAY_MODEL, + drawInFront: false, + visible: false + }); + + _this.teleportedTargetOverlay = Overlays.addOverlay("model", { + url: TARGET_MODEL_URL, + alpha: _this.TELEPORTED_TARGET_ALPHA, + visible: false + }); + + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.playAreaOverlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.teleportedTargetOverlay); + + + _this.playArea = HMD.playArea; + _this.isPlayAreaAvailable = HMD.active && _this.playArea.width !== 0 && _this.playArea.height !== 0; + if (_this.isPlayAreaAvailable) { + _this.playAreaCenterOffset = Vec3.sum({ x: _this.playArea.x, y: 0, z: _this.playArea.y }, + _this.PLAY_AREA_OVERLAY_OFFSET); + _this.playAreaSensorPositions = HMD.sensorPositions; + + for (var i = 0; i < _this.playAreaSensorPositions.length; i++) { + if (i > _this.playAreaSensorPositionOverlays.length - 1) { + var overlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_SENSOR_OVERLAY_MODEL, + dimensions: _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS, + parentID: _this.playAreaOverlay, + localRotation: _this.PLAY_AREA_SENSOR_OVERLAY_ROTATION, + drawInFront: false, + visible: false + }); + _this.playAreaSensorPositionOverlays.push(overlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", overlay); + } + } + + _this.setPlayAreaDimensions(); + } + _this.init = true; - } + }; _this.initPointers(); + + this.translateXAction = Controller.findAction("TranslateX"); + this.translateYAction = Controller.findAction("TranslateY"); + this.translateZAction = Controller.findAction("TranslateZ"); + + this.setPlayAreaVisible = function (visible, targetOverlayID, fade) { + if (!this.isPlayAreaAvailable || this.isPlayAreaVisible === visible) { + return; + } + + this.wasPlayAreaVisible = this.isPlayAreaVisible; + this.isPlayAreaVisible = visible; + this.targetOverlayID = targetOverlayID; + + if (this.teleportedFadeTimer !== null) { + Script.clearTimeout(this.teleportedFadeTimer); + this.teleportedFadeTimer = null; + } + if (visible || !fade) { + // Immediately make visible or invisible. + this.isPlayAreaVisible = visible; + Overlays.editOverlay(this.playAreaOverlay, { + dimensions: Vec3.ZERO, + alpha: this.PLAY_AREA_BOX_ALPHA, + visible: visible + }); + for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) { + Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.ZERO, + alpha: this.PLAY_AREA_SENSOR_ALPHA, + visible: visible + }); + } + Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false }); + } else { + // Fading out of overlays is initiated in setTeleportVisible(). + } + }; + + this.updatePlayArea = function (position) { + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetXZPosition = { x: position.x, y: 0, z: position.z }; + var avatarXZPosition = MyAvatar.position; + avatarXZPosition.y = 0; + var MIN_PARENTING_DISTANCE = 0.2; // Parenting under this distance results in the play area's rotation jittering. + if (Vec3.distance(targetXZPosition, avatarXZPosition) < MIN_PARENTING_DISTANCE) { + // Set play area position and rotation in world coordinates with no parenting. + Overlays.editOverlay(this.playAreaOverlay, { + parentID: Uuid.NULL, + position: Vec3.sum(position, + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(this.playAreaCenterOffset, avatarSensorPosition)))), + rotation: sensorToWorldRotation + }); + } else { + // Set play area position and rotation in local coordinates with parenting. + var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation"); + var sensorToTargetRotation = Quat.multiply(Quat.inverse(targetRotation), sensorToWorldRotation); + var relativePlayAreaCenterOffset = + Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + Overlays.editOverlay(this.playAreaOverlay, { + parentID: this.targetOverlayID, + localPosition: Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))), + localRotation: sensorToTargetRotation + }); + } + }; + + + this.scaleInTeleport = function () { + _this.teleportScaleFactor = Math.min((Date.now() - _this.teleportScaleStart) / _this.TELEPORT_SCALE_DURATION, 1); + Pointers.editRenderState( + _this.teleportScaleMode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.multiply(_this.teleportScaleFactor, TARGET_MODEL_DIMENSIONS) } + } + ); + if (_this.isPlayAreaVisible) { + _this.setPlayAreaDimensions(); + } + if (_this.teleportScaleFactor < 1) { + _this.teleportScaleTimer = Script.setTimeout(_this.scaleInTeleport, _this.TELEPORT_SCALE_TIMEOUT); + } else { + _this.teleportScaleTimer = null; + } + }; + + this.fadeOutTeleport = function () { + var isAvatarMoving, + i, length; + + isAvatarMoving = Controller.getActionValue(_this.translateXAction) !== 0 + || Controller.getActionValue(_this.translateYAction) !== 0 + || Controller.getActionValue(_this.translateZAction) !== 0; + + if (_this.teleportedFadeDelayFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Delay fade. + _this.teleportedFadeDelayFactor = _this.teleportedFadeDelayFactor - _this.TELEPORTED_FADE_DELAY_DELTA; + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else if (_this.teleportedFadeFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Fade. + _this.teleportedFadeFactor = _this.teleportedFadeFactor - _this.TELEPORTED_FADE_DELTA; + Overlays.editOverlay(_this.teleportedTargetOverlay, { + alpha: _this.teleportedFadeFactor * _this.TELEPORTED_TARGET_ALPHA + }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { + alpha: _this.teleportedFadeFactor * _this.PLAY_AREA_BOX_ALPHA + }); + var sensorAlpha = _this.teleportedFadeFactor * _this.PLAY_AREA_SENSOR_ALPHA; + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { alpha: sensorAlpha }); + } + } + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else { + // Make invisible. + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { visible: false }); + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + _this.teleportedFadeTimer = null; + Selection.disableListHighlight(this.teleporterSelectionName); + } + }; + + this.cancelFade = function () { + // Other hand may call this to immediately hide fading overlays. + var i, length; + if (this.teleportedFadeTimer) { + Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false }); + if (this.wasPlayAreaVisible) { + Overlays.editOverlay(this.playAreaOverlay, { visible: false }); + for (i = 0, length = this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + this.teleportedFadeTimer = null; + } + }; + + this.setTeleportVisible = function (visible, mode, fade) { + // Scales in teleport target and play area when start displaying them. + if (visible === this.isTeleportVisible) { + return; + } + + if (visible) { + this.teleportScaleMode = mode; + Pointers.editRenderState( + mode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.ZERO } + } + ); + this.getOtherModule().cancelFade(); + this.teleportScaleStart = Date.now(); + this.teleportScaleFactor = 0; + this.scaleInTeleport(); + Selection.enableListHighlight(this.teleporterSelectionName, this.TELEPORTER_SELECTION_STYLE); + } else { + if (this.teleportScaleTimer !== null) { + Script.clearTimeout(this.teleportScaleTimer); + this.teleportScaleTimer = null; + } + + if (fade) { + // Copy of target at teleported position for fading. + var avatarScale = MyAvatar.sensorToWorldScale; + Overlays.editOverlay(this.teleportedTargetOverlay, { + position: Vec3.sum(this.teleportedPosition, { + x: 0, + y: -getAvatarFootOffset() + avatarScale * TARGET_MODEL_DIMENSIONS.y / 2, + z: 0 + }), + rotation: Quat.multiply(this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation), + dimensions: Vec3.multiply(avatarScale, TARGET_MODEL_DIMENSIONS), + alpha: this.TELEPORTED_TARGET_ALPHA, + visible: true + }); + + // Fade out over time. + this.teleportedFadeDelayFactor = 1.0; + this.teleportedFadeFactor = 1.0; + this.teleportedFadeTimer = Script.setTimeout(this.fadeOutTeleport, this.TELEPORTED_FADE_DELAY); + } else { + Selection.disableListHighlight(this.teleporterSelectionName); + } + } + + this.isTeleportVisible = visible; + }; + + this.axisButtonStateX = 0; // Left/right axis button pressed. this.axisButtonStateY = 0; // Up/down axis button pressed. this.BUTTON_TRANSITION_DELAY = 100; // Allow time for transition from direction buttons to touch-pad. @@ -379,6 +764,7 @@ Script.include("/~/system/libraries/controllers.js"); this.setTeleportState(mode, "cancel", "collision"); } else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) { this.setTeleportState(mode, "teleport", "collision"); + this.updatePlayArea(result.intersection); } else if (teleportLocationType === TARGET.SEAT) { this.setTeleportState(mode, "collision", "seat"); } @@ -387,6 +773,7 @@ Script.include("/~/system/libraries/controllers.js"); this.teleport = function(newResult, target) { var result = newResult; + this.teleportedPosition = newResult.intersection; if (_this.buttonValue !== 0) { return makeRunningValues(true, [], []); } @@ -410,6 +797,8 @@ Script.include("/~/system/libraries/controllers.js"); }; this.disableLasers = function() { + this.setPlayAreaVisible(false, null, true); + this.setTeleportVisible(false, null, true); Pointers.disablePointer(_this.teleportParabolaHandVisuals); Pointers.disablePointer(_this.teleportParabolaHandCollisions); Pointers.disablePointer(_this.teleportParabolaHeadVisuals); @@ -418,14 +807,29 @@ Script.include("/~/system/libraries/controllers.js"); Picks.disablePick(_this.teleportHandCollisionPick); }; - this.setTeleportState = function(mode, visibleState, invisibleState) { + this.teleportState = ""; + + this.setTeleportState = function (mode, visibleState, invisibleState) { + var teleportState = mode + visibleState + invisibleState; + if (teleportState === this.teleportState) { + return; + } + this.teleportState = teleportState; + + var pointerID; if (mode === 'head') { Pointers.setRenderState(_this.teleportParabolaHeadVisuals, visibleState); Pointers.setRenderState(_this.teleportParabolaHeadCollisions, invisibleState); + pointerID = _this.teleportParabolaHeadVisuals; } else { Pointers.setRenderState(_this.teleportParabolaHandVisuals, visibleState); Pointers.setRenderState(_this.teleportParabolaHandCollisions, invisibleState); + pointerID = _this.teleportParabolaHandVisuals; } + var visible = visibleState === "teleport"; + this.setPlayAreaVisible(visible && MyAvatar.showPlayArea, + Pointers.getPointerProperties(pointerID).renderStates.teleport.end, false); + this.setTeleportVisible(visible, mode, false); }; this.setIgnoreEntities = function(entitiesToIgnore) { @@ -642,4 +1046,9 @@ Script.include("/~/system/libraries/controllers.js"); Messages.subscribe('Hifi-Teleport-Ignore-Remove'); Messages.messageReceived.connect(handleTeleportMessages); + MyAvatar.sensorToWorldScaleChanged.connect(function () { + leftTeleporter.updatePlayAreaScale(); + rightTeleporter.updatePlayAreaScale(); + }); + }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/handTouch.js b/scripts/system/controllers/handTouch.js index db79aa4a77..97a24cb3f2 100644 --- a/scripts/system/controllers/handTouch.js +++ b/scripts/system/controllers/handTouch.js @@ -16,7 +16,9 @@ (function () { + var LEAP_MOTION_NAME = "LeapMotion"; var handTouchEnabled = true; + var leapMotionEnabled = Controller.getRunningInputDeviceNames().indexOf(LEAP_MOTION_NAME) >= 0; var MSECONDS_AFTER_LOAD = 2000; var updateFingerWithIndex = 0; var untouchableEntities = []; @@ -870,6 +872,12 @@ handTouchEnabled = !shouldDisable; }); + Controller.inputDeviceRunningChanged.connect(function (deviceName, isEnabled) { + if (deviceName == LEAP_MOTION_NAME) { + leapMotionEnabled = isEnabled; + } + }); + MyAvatar.disableHandTouchForIDChanged.connect(function (entityID, disable) { var entityIndex = untouchableEntities.indexOf(entityID); if (disable) { @@ -902,7 +910,7 @@ Script.update.connect(function () { - if (!handTouchEnabled) { + if (!handTouchEnabled || leapMotionEnabled) { return; } diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 89974f9409..7eed78ecf3 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -15,6 +15,7 @@ + diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 7cc5937536..fed4dfb632 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -15,9 +15,31 @@ const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; const BAKED_GLYPH = "" const SCRIPT_GLYPH = "k"; +const BYTES_PER_MEGABYTE = 1024 * 1024; +const IMAGE_MODEL_NAME = 'default-image-model.fbx'; +const COLLAPSE_EXTRA_INFO = "E"; +const EXPAND_EXTRA_INFO = "D"; +const FILTER_IN_VIEW_ATTRIBUTE = "pressed"; +const WINDOW_NONVARIABLE_HEIGHT = 207; +const NUM_COLUMNS = 12; +const EMPTY_ENTITY_ID = "0"; const DELETE = 46; // Key code for the delete key. const KEY_P = 80; // Key code for letter p used for Parenting hotkey. -const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities. + +const COLUMN_INDEX = { + TYPE: 0, + NAME: 1, + URL: 2, + LOCKED: 3, + VISIBLE: 4, + VERTICLES_COUNT: 5, + TEXTURES_COUNT: 6, + TEXTURES_SIZE: 7, + HAS_TRANSPARENT: 8, + IS_BAKED: 9, + DRAW_CALLS: 10, + HAS_SCRIPT: 11 +}; const COMPARE_ASCENDING = function(a, b) { let va = a[currentSortColumn]; @@ -37,17 +59,21 @@ const COMPARE_DESCENDING = function(a, b) { return COMPARE_ASCENDING(b, a); } - // List of all entities -let entities = [] +var entities = [] // List of all entities, indexed by Entity ID var entitiesByID = {}; -// The filtered and sorted list of entities +// The filtered and sorted list of entities passed to ListView var visibleEntities = []; - +// List of all entities that are currently selected var selectedEntities = []; + +var entityList = null; // The ListView + var currentSortColumn = 'type'; var currentSortOrder = ASCENDING_SORT; +var isFilterInView = false; +var showExtraInfo = false; const ENABLE_PROFILING = false; var profileIndent = ''; @@ -56,19 +82,25 @@ const PROFILE_NOOP = function(_name, fn, args) { } ; const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) { console.log("PROFILE-Web " + profileIndent + "(" + name + ") Begin"); - var previousIndent = profileIndent; + let previousIndent = profileIndent; profileIndent += ' '; - var before = Date.now(); + let before = Date.now(); fn.apply(this, args); - var delta = Date.now() - before; + let delta = Date.now() - before; profileIndent = previousIndent; console.log("PROFILE-Web " + profileIndent + "(" + name + ") End " + delta + "ms"); }; +debugPrint = function (message) { + console.log(message); +}; + function loaded() { openEventBridge(function() { elEntityTable = document.getElementById("entity-table"); elEntityTableBody = document.getElementById("entity-table-body"); + elEntityTableScroll = document.getElementById("entity-table-scroll"); + elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th"); elRefresh = document.getElementById("refresh"); elToggleLocked = document.getElementById("locked"); elToggleVisible = document.getElementById("visible"); @@ -78,15 +110,13 @@ function loaded() { elRadius = document.getElementById("radius"); elExport = document.getElementById("export"); elPal = document.getElementById("pal"); - elEntityTable = document.getElementById("entity-table"); elInfoToggle = document.getElementById("info-toggle"); elInfoToggleGlyph = elInfoToggle.firstChild; elFooter = document.getElementById("footer-text"); elNoEntitiesMessage = document.getElementById("no-entities"); elNoEntitiesInView = document.getElementById("no-entities-in-view"); elNoEntitiesRadius = document.getElementById("no-entities-radius"); - elEntityTableScroll = document.getElementById("entity-table-scroll"); - + document.getElementById("entity-name").onclick = function() { setSortColumn('name'); }; @@ -123,14 +153,46 @@ function loaded() { document.getElementById("entity-hasScript").onclick = function () { setSortColumn('hasScript'); }; - + elRefresh.onclick = function() { + refreshEntities(); + } + elToggleLocked.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); + } + elToggleVisible.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); + } + elExport.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); + } + elPal.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); + } + elDelete.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + } + elFilter.onkeyup = refreshEntityList; + elFilter.onpaste = refreshEntityList; + elFilter.onchange = onFilterChange; + elFilter.onblur = refreshFooter; + elInView.onclick = toggleFilterInView; + elRadius.onchange = onRadiusChange; + elInfoToggle.onclick = toggleInfo; + + elNoEntitiesInView.style.display = "none"; + + entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, + createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); + function onRowClicked(clickEvent) { let entityID = this.dataset.entityID; let selection = [entityID]; + if (clickEvent.ctrlKey) { let selectedIndex = selectedEntities.indexOf(entityID); if (selectedIndex >= 0) { - selection = selectedEntities; + selection = []; + selection = selection.concat(selectedEntities); selection.splice(selectedIndex, 1) } else { selection = selection.concat(selectedEntities); @@ -145,35 +207,26 @@ function loaded() { } else if (previousItemFound === -1 && selectedEntities[0] === entity.id) { previousItemFound = i; } - }; + } if (previousItemFound !== -1 && clickedItemFound !== -1) { - let betweenItems = []; + selection = []; let toItem = Math.max(previousItemFound, clickedItemFound); - // skip first and last item in this loop, we add them to selection after the loop - for (let i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) { - visibleEntities[i].el.className = 'selected'; - betweenItems.push(visibleEntities[i].id); + for (let i = Math.min(previousItemFound, clickedItemFound); i <= toItem; i++) { + selection.push(visibleEntities[i].id); } if (previousItemFound > clickedItemFound) { // always make sure that we add the items in the right order - betweenItems.reverse(); + selection.reverse(); } - selection = selection.concat(betweenItems, selectedEntities); + } + } else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) { + // if reselecting the same entity then deselect it + if (selectedEntities[0] === entityID) { + selection = []; } } - - selectedEntities.forEach(function(entityID) { - if (selection.indexOf(entityID) === -1) { - let entity = entitiesByID[entityID]; - if (entity !== undefined) { - entity.el.className = ''; - } - } - }); - - selectedEntities = selection; - - this.className = 'selected'; + + updateSelectedEntities(selection); EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", @@ -191,9 +244,7 @@ function loaded() { entityIds: [this.dataset.entityID], })); } - - const BYTES_PER_MEGABYTE = 1024 * 1024; - + function decimalMegabytes(number) { return number ? (number / BYTES_PER_MEGABYTE).toFixed(1) : ""; } @@ -206,15 +257,12 @@ function loaded() { let urlParts = url.split('/'); return urlParts[urlParts.length - 1]; } - - // Update the entity list with the new set of data sent from edit.js - function updateEntityList(entityData) { - const IMAGE_MODEL_NAME = 'default-image-model.fbx'; - - entities = [] + + function updateEntityData(entityData) { + entities = []; entitiesByID = {}; - visibleEntities = []; - + visibleEntities.length = 0; // maintains itemData reference in ListView + PROFILE("map-data", function() { entityData.forEach(function(entity) { let type = entity.type; @@ -222,7 +270,7 @@ function loaded() { if (filename === IMAGE_MODEL_NAME) { type = "Image"; } - + let entityData = { id: entity.id, name: entity.name, @@ -231,60 +279,25 @@ function loaded() { fullUrl: entity.url, locked: entity.locked, visible: entity.visible, - verticesCount: entity.verticesCount, - texturesCount: entity.texturesCount, - texturesSize: entity.texturesSize, + verticesCount: displayIfNonZero(entity.verticesCount), + texturesCount: displayIfNonZero(entity.texturesCount), + texturesSize: decimalMegabytes(entity.texturesSize), hasTransparent: entity.hasTransparent, isBaked: entity.isBaked, - drawCalls: entity.drawCalls, + drawCalls: displayIfNonZero(entity.drawCalls), hasScript: entity.hasScript, + elRow: null, // if this entity has a visible row element assigned to it + selected: false // if this entity is selected for edit regardless of having a visible row } - + entities.push(entityData); entitiesByID[entityData.id] = entityData; }); }); - - PROFILE("create-rows", function() { - entities.forEach(function(entity) { - let row = document.createElement('tr'); - row.dataset.entityID = entity.id; - row.attributes.title = entity.fullUrl; - function addColumn(cls, text) { - let col = document.createElement('td'); - col.className = cls; - col.innerText = text; - row.append(col); - } - function addColumnHTML(cls, text) { - let col = document.createElement('td'); - col.className = cls; - col.innerHTML = text; - row.append(col); - } - addColumn('type', entity.type); - addColumn('name', entity.name); - addColumn('url', entity.url); - addColumnHTML('locked glyph', entity.locked ? LOCKED_GLYPH : null); - addColumnHTML('visible glyph', entity.visible ? VISIBLE_GLYPH : null); - addColumn('verticesCount', displayIfNonZero(entity.verticesCount)); - addColumn('texturesCount', displayIfNonZero(entity.texturesCount)); - addColumn('texturesSize', decimalMegabytes(entity.texturesSize)); - addColumnHTML('hasTransparent glyph', entity.hasTransparent ? TRANSPARENCY_GLYPH : null); - addColumnHTML('isBaked glyph', entity.isBaked ? BAKED_GLYPH : null); - addColumn('drawCalls', displayIfNonZero(entity.drawCalls)); - addColumn('hasScript glyph', entity.hasScript ? SCRIPT_GLYPH : null); - row.addEventListener('click', onRowClicked); - row.addEventListener('dblclick', onRowDoubleClicked); - - entity.el = row; - }); - }); - + refreshEntityList(); - updateSelectedEntities(selectedEntities); } - + function refreshEntityList() { PROFILE("refresh-entity-list", function() { PROFILE("filter", function() { @@ -300,44 +313,95 @@ function loaded() { }); } }); - + PROFILE("sort", function() { let cmp = currentSortOrder === ASCENDING_SORT ? COMPARE_ASCENDING : COMPARE_DESCENDING; visibleEntities.sort(cmp); }); PROFILE("update-dom", function() { - elEntityTableBody.innerHTML = ''; - for (let i = 0, len = visibleEntities.length; i < len; ++i) { - elEntityTableBody.append(visibleEntities[i].el); - } + entityList.itemData = visibleEntities; + entityList.refresh(); }); + + refreshFooter(); + refreshNoEntitiesMessage(); }); } - + function removeEntities(deletedIDs) { // Loop from the back so we can pop items off while iterating + + // delete any entities matching deletedIDs list from entities and entitiesByID lists + // if the entity had an associated row element then ensure row is unselected and clear it's entity for (let j = entities.length - 1; j >= 0; --j) { - let id = entities[j]; + let id = entities[j].id; for (let i = 0, length = deletedIDs.length; i < length; ++i) { if (id === deletedIDs[i]) { + let elRow = entities[j].elRow; + if (elRow) { + elRow.className = ''; + elRow.dataset.entityID = EMPTY_ENTITY_ID; + } entities.splice(j, 1); - entitiesByID[id].el.remove(); delete entitiesByID[id]; break; } } } - refreshEntities(); - } - - function clearEntities() { - entities = [] - entitiesByID = {}; - visibleEntities = []; - elEntityTableBody.innerHTML = ''; + + // delete any entities matching deletedIDs list from selectedEntities list + for (let j = selectedEntities.length - 1; j >= 0; --j) { + let id = selectedEntities[j].id; + for (let i = 0, length = deletedIDs.length; i < length; ++i) { + if (id === deletedIDs[i]) { + selectedEntities.splice(j, 1); + break; + } + } + } + // delete any entities matching deletedIDs list from visibleEntities list + // if this was a row that was above our current row offset (a hidden top row in the top buffer), + // then decrease row offset accordingly + let firstVisibleRow = entityList.getFirstVisibleRowIndex(); + for (let j = visibleEntities.length - 1; j >= 0; --j) { + let id = visibleEntities[j].id; + for (let i = 0, length = deletedIDs.length; i < length; ++i) { + if (id === deletedIDs[i]) { + if (j < firstVisibleRow && entityList.rowOffset > 0) { + entityList.rowOffset--; + } + visibleEntities.splice(j, 1); + break; + } + } + } + + entityList.refresh(); + refreshFooter(); + refreshNoEntitiesMessage(); + } + + function clearEntities() { + // clear the associated entity ID from all visible row elements + let firstVisibleRow = entityList.getFirstVisibleRowIndex(); + let lastVisibleRow = entityList.getLastVisibleRowIndex(); + for (let i = firstVisibleRow; i <= lastVisibleRow && i < visibleEntities.length; i++) { + let entity = visibleEntities[i]; + entity.elRow.dataset.entityID = EMPTY_ENTITY_ID; + } + + entities = []; + entitiesByID = {}; + visibleEntities.length = 0; // maintains itemData reference in ListView + + entityList.resetToTop(); + entityList.clear(); + + refreshFooter(); + refreshNoEntitiesMessage(); } var elSortOrder = { @@ -363,17 +427,18 @@ function loaded() { currentSortColumn = column; currentSortOrder = ASCENDING_SORT; } - elSortOrder[column].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING; + refreshSortOrder(); refreshEntityList(); }); } - setSortColumn('type'); - + function refreshSortOrder() { + elSortOrder[currentSortColumn].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING; + } + function refreshEntities() { - clearEntities(); EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); } - + function refreshFooter() { if (selectedEntities.length > 1) { elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; @@ -385,76 +450,142 @@ function loaded() { elFooter.firstChild.nodeValue = visibleEntities.length + " entities found"; } } - - + + function refreshNoEntitiesMessage() { + if (visibleEntities.length > 0) { + elNoEntitiesMessage.style.display = "none"; + } else { + elNoEntitiesMessage.style.display = "block"; + } + } + function updateSelectedEntities(selectedIDs) { let notFound = false; - + + // reset all currently selected entities and their rows first selectedEntities.forEach(function(id) { let entity = entitiesByID[id]; if (entity !== undefined) { - entity.el.className = ''; + entity.selected = false; + if (entity.elRow) { + entity.elRow.className = ''; + } } }); - + + // then reset selected entities list with newly selected entities and set them selected selectedEntities = []; - for (let i = 0; i < selectedIDs.length; i++) { - let id = selectedIDs[i]; + selectedIDs.forEach(function(id) { selectedEntities.push(id); let entity = entitiesByID[id]; if (entity !== undefined) { - entity.el.className = 'selected'; + entity.selected = true; + if (entity.elRow) { + entity.elRow.className = 'selected'; + } } else { notFound = true; } - } + }); refreshFooter(); return notFound; } - - elRefresh.onclick = function() { - refreshEntities(); + + function isGlyphColumn(columnIndex) { + return columnIndex === COLUMN_INDEX.LOCKED || columnIndex === COLUMN_INDEX.VISIBLE || + columnIndex === COLUMN_INDEX.HAS_TRANSPARENT || columnIndex === COLUMN_INDEX.IS_BAKED || + columnIndex === COLUMN_INDEX.HAS_SCRIPT; } - elToggleLocked.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); - } - elToggleVisible.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); - } - elExport.onclick = function() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); - } - elPal.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); - } - elDelete.onclick = function() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - } - - document.addEventListener("keydown", function (keyDownEvent) { - if (keyDownEvent.target.nodeName === "INPUT") { - return; + + function createRow() { + let row = document.createElement("tr"); + for (let i = 0; i < NUM_COLUMNS; i++) { + let column = document.createElement("td"); + if (isGlyphColumn(i)) { + column.className = 'glyph'; + } + row.appendChild(column); } - var keyCode = keyDownEvent.keyCode; - if (keyCode === DELETE) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - refreshEntities(); + row.onclick = onRowClicked; + row.ondblclick = onRowDoubleClicked; + return row; + } + + function updateRow(elRow, itemData) { + // update all column texts and glyphs to this entity's data + let typeCell = elRow.childNodes[COLUMN_INDEX.TYPE]; + typeCell.innerText = itemData.type; + let nameCell = elRow.childNodes[COLUMN_INDEX.NAME]; + nameCell.innerText = itemData.name; + let urlCell = elRow.childNodes[COLUMN_INDEX.URL]; + urlCell.innerText = itemData.url; + let lockedCell = elRow.childNodes[COLUMN_INDEX.LOCKED]; + lockedCell.innerHTML = itemData.locked ? LOCKED_GLYPH : null; + let visibleCell = elRow.childNodes[COLUMN_INDEX.VISIBLE]; + visibleCell.innerHTML = itemData.visible ? VISIBLE_GLYPH : null; + let verticesCountCell = elRow.childNodes[COLUMN_INDEX.VERTICLES_COUNT]; + verticesCountCell.innerText = itemData.verticesCount; + let texturesCountCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_COUNT]; + texturesCountCell.innerText = itemData.texturesCount; + let texturesSizeCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_SIZE]; + texturesSizeCell.innerText = itemData.texturesSize; + let hasTransparentCell = elRow.childNodes[COLUMN_INDEX.HAS_TRANSPARENT]; + hasTransparentCell.innerHTML = itemData.hasTransparent ? TRANSPARENCY_GLYPH : null; + let isBakedCell = elRow.childNodes[COLUMN_INDEX.IS_BAKED]; + isBakedCell.innerHTML = itemData.isBaked ? BAKED_GLYPH : null; + let drawCallsCell = elRow.childNodes[COLUMN_INDEX.DRAW_CALLS]; + drawCallsCell.innerText = itemData.drawCalls; + let hasScriptCell = elRow.childNodes[COLUMN_INDEX.HAS_SCRIPT]; + hasScriptCell.innerHTML = itemData.hasScript ? SCRIPT_GLYPH : null; + + // if this entity was previously selected flag it's row as selected + if (itemData.selected) { + elRow.className = 'selected'; + } else { + elRow.className = ''; } - if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) { - if (keyDownEvent.shiftKey) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + + // if this row previously had an associated entity ID that wasn't the new entity ID then clear + // the ID from the row and the row element from the previous entity's data, then set the new + // entity ID to the row and the row element to the new entity's data + let prevEntityID = elRow.dataset.entityID; + let newEntityID = itemData.id; + let validPrevItemID = prevEntityID !== undefined && prevEntityID !== EMPTY_ENTITY_ID; + if (validPrevItemID && prevEntityID !== newEntityID && entitiesByID[prevEntityID].elRow === elRow) { + elRow.dataset.entityID = EMPTY_ENTITY_ID; + entitiesByID[prevEntityID].elRow = null; + } + if (!validPrevItemID || prevEntityID !== newEntityID) { + elRow.dataset.entityID = newEntityID; + entitiesByID[newEntityID].elRow = elRow; + } + } + + function clearRow(elRow) { + // reset all texts and glyphs for each of the row's column + for (let i = 0; i < NUM_COLUMNS; i++) { + let cell = elRow.childNodes[i]; + if (isGlyphColumn(i)) { + cell.innerHTML = ""; } else { - EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + cell.innerText = ""; } } - }, false); - - var isFilterInView = false; - var FILTER_IN_VIEW_ATTRIBUTE = "pressed"; - elNoEntitiesInView.style.display = "none"; - elInView.onclick = function () { + + // clear the row from any associated entity + let entityID = elRow.dataset.entityID; + if (entityID && entitiesByID[entityID]) { + entitiesByID[entityID].elRow = null; + } + + // reset the row to hidden and clear the entity from the row + elRow.className = ''; + elRow.dataset.entityID = EMPTY_ENTITY_ID; + } + + function toggleFilterInView() { isFilterInView = !isFilterInView; if (isFilterInView) { elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); @@ -466,100 +597,20 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView })); refreshEntities(); } - - elRadius.onchange = function () { + + function onFilterChange() { + refreshEntityList(); + entityList.resize(); + } + + function onRadiusChange() { elRadius.value = Math.max(elRadius.value, 0); + elNoEntitiesRadius.firstChild.nodeValue = elRadius.value; + elNoEntitiesMessage.style.display = "none"; EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value })); refreshEntities(); - elNoEntitiesRadius.firstChild.nodeValue = elRadius.value; } - if (window.EventBridge !== undefined) { - EventBridge.scriptEventReceived.connect(function(data) { - data = JSON.parse(data); - - if (data.type === "clearEntityList") { - clearEntities(); - } else if (data.type == "selectionUpdate") { - var notFound = updateSelectedEntities(data.selectedIDs); - if (notFound) { - refreshEntities(); - } - } else if (data.type === "update" && data.selectedIDs !== undefined) { - PROFILE("update", function() { - var newEntities = data.entities; - if (newEntities && newEntities.length === 0) { - elNoEntitiesMessage.style.display = "block"; - elFooter.firstChild.nodeValue = "0 entities found"; - } else if (newEntities) { - elNoEntitiesMessage.style.display = "none"; - updateEntityList(newEntities); - updateSelectedEntities(data.selectedIDs); - } - }); - } else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) { - removeEntities(data.deletedIDs); - updateSelectedEntities(data.selectedIDs); - } else if (data.type === "deleted" && data.ids) { - removeEntities(data.ids); - refreshFooter(); - } - }); - setTimeout(refreshEntities, 1000); - } - - function resize() { - // Take up available window space - elEntityTableScroll.style.height = window.innerHeight - 207; - - var SCROLLABAR_WIDTH = 21; - var tds = document.querySelectorAll("#entity-table-body tr:first-child td"); - var ths = document.querySelectorAll("#entity-table thead th"); - if (tds.length >= ths.length) { - // Update the widths of the header cells to match the body - for (var i = 0; i < ths.length; i++) { - ths[i].width = tds[i].offsetWidth; - } - } else { - // Reasonable widths if nothing is displayed - var tableWidth = document.getElementById("entity-table").offsetWidth - SCROLLABAR_WIDTH; - if (showExtraInfo) { - ths[0].width = 0.10 * tableWidth; - ths[1].width = 0.20 * tableWidth; - ths[2].width = 0.20 * tableWidth; - ths[3].width = 0.04 * tableWidth; - ths[4].width = 0.04 * tableWidth; - ths[5].width = 0.08 * tableWidth; - ths[6].width = 0.08 * tableWidth; - ths[7].width = 0.10 * tableWidth; - ths[8].width = 0.04 * tableWidth; - ths[9].width = 0.08 * tableWidth; - ths[10].width = 0.04 * tableWidth + SCROLLABAR_WIDTH; - } else { - ths[0].width = 0.16 * tableWidth; - ths[1].width = 0.34 * tableWidth; - ths[2].width = 0.34 * tableWidth; - ths[3].width = 0.08 * tableWidth; - ths[4].width = 0.08 * tableWidth; - } - } - }; - - window.onresize = resize; - - elFilter.onkeyup = refreshEntityList; - elFilter.onpaste = refreshEntityList; - elFilter.onchange = function() { - refreshEntityList(); - resize(); - }; - elFilter.onblur = refreshFooter; - - - var showExtraInfo = false; - var COLLAPSE_EXTRA_INFO = "E"; - var EXPAND_EXTRA_INFO = "D"; - function toggleInfo(event) { showExtraInfo = !showExtraInfo; if (showExtraInfo) { @@ -569,15 +620,62 @@ function loaded() { elEntityTable.className = ""; elInfoToggleGlyph.innerHTML = EXPAND_EXTRA_INFO; } - resize(); + entityList.resize(); event.stopPropagation(); } - elInfoToggle.addEventListener("click", toggleInfo, true); - - resize(); + document.addEventListener("keydown", function (keyDownEvent) { + if (keyDownEvent.target.nodeName === "INPUT") { + return; + } + let keyCode = keyDownEvent.keyCode; + if (keyCode === DELETE) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + } + if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) { + if (keyDownEvent.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } + }, false); + + if (window.EventBridge !== undefined) { + EventBridge.scriptEventReceived.connect(function(data) { + data = JSON.parse(data); + if (data.type === "clearEntityList") { + clearEntities(); + } else if (data.type == "selectionUpdate") { + let notFound = updateSelectedEntities(data.selectedIDs); + if (notFound) { + refreshEntities(); + } + } else if (data.type === "update" && data.selectedIDs !== undefined) { + PROFILE("update", function() { + let newEntities = data.entities; + if (newEntities) { + if (newEntities.length === 0) { + clearEntities(); + } else { + updateEntityData(newEntities); + updateSelectedEntities(data.selectedIDs); + } + } + }); + } else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) { + removeEntities(data.deletedIDs); + updateSelectedEntities(data.selectedIDs); + } else if (data.type === "deleted" && data.ids) { + removeEntities(data.ids); + } + }); + } + + refreshSortOrder(); + refreshEntities(); }); - + augmentSpinButtons(); // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js new file mode 100644 index 0000000000..10dc37efba --- /dev/null +++ b/scripts/system/html/js/listView.js @@ -0,0 +1,307 @@ +// listView.js +// +// Created by David Back on 27 Aug 2018 +// Copyright 2018 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 + +const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scroll +const FIRST_ROW_INDEX = 3; // the first elRow element's index in the child nodes of the table body + +debugPrint = function (message) { + console.log(message); +}; + +function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, + updateRowFunction, clearRowFunction, WINDOW_NONVARIABLE_HEIGHT) { + this.elTableBody = elTableBody; + this.elTableScroll = elTableScroll; + this.elTableHeaderRow = elTableHeaderRow; + + this.elTopBuffer = null; + this.elBottomBuffer = null; + + this.createRowFunction = createRowFunction; + this.updateRowFunction = updateRowFunction; + this.clearRowFunction = clearRowFunction; + + // the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer + this.elRows = []; + // the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row + this.itemData = []; + // the current index within the itemData list that is set to the top most elRow element + this.rowOffset = 0; + // height of the elRow elements + this.rowHeight = 0; + // the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling + this.lastRowShiftScrollTop = 0; + + this.initialize(); +}; + +ListView.prototype = { + getNumRows: function() { + return this.elRows.length; + }, + + getScrollHeight: function() { + return this.rowHeight * SCROLL_ROWS; + }, + + getFirstVisibleRowIndex: function() { + return this.rowOffset; + }, + + getLastVisibleRowIndex: function() { + return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1; + }, + + resetToTop: function() { + this.rowOffset = 0; + this.lastRowShiftScrollTop = 0; + this.refreshBuffers(); + this.elTableScroll.scrollTop = 0; + }, + + clear: function() { + for (let i = 0; i < this.getNumRows(); i++) { + let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX]; + this.clearRowFunction(elRow); + elRow.style.display = "none"; // hide cleared rows + } + }, + + onScroll: function() { + var that = this.listView; + that.scroll(); + }, + + scroll: function() { + let scrollTop = this.elTableScroll.scrollTop; + let scrollHeight = this.getScrollHeight(); + let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight; + let totalItems = this.itemData.length; + let numRows = this.getNumRows(); + + // if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there + // are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of + // remaining rows below + // if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of + // the scroll row space or number of rows above + if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) { + let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight); + let numScrollRows = numScrolls * SCROLL_ROWS; + if (numScrollRows + this.rowOffset + numRows > totalItems) { + numScrollRows = totalItems - this.rowOffset - numRows; + } + this.scrollRows(numScrollRows); + } else if (scrollTop < this.lastRowShiftScrollTop) { + let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight); + let numScrollRows = numScrolls * SCROLL_ROWS; + if (this.rowOffset - numScrollRows < 0) { + numScrollRows = this.rowOffset; + } + this.scrollRows(-numScrollRows); + } + }, + + scrollRows: function(numScrollRows) { + let numScrollRowsAbsolute = Math.abs(numScrollRows); + if (numScrollRowsAbsolute === 0) { + return; + } + + let scrollDown = numScrollRows > 0; + + let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height")); + let prevBottomHeight = parseInt(this.elBottomBuffer.getAttribute("height")); + + // if the number of rows to scroll at once is greater than the total visible number of row elements, + // then just advance the rowOffset accordingly and allow the refresh below to update all rows + if (numScrollRowsAbsolute > this.getNumRows()) { + this.rowOffset += numScrollRows; + } else { + // for each row to scroll down, move the top row element to the bottom of the + // table before the bottom buffer and reset it's row data to the new item + // for each row to scroll up, move the bottom row element to the top of + // the table before the top row and reset it's row data to the new item + for (let i = 0; i < numScrollRowsAbsolute; i++) { + let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX]; + let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1]; + let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1; + let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow; + this.elTableBody.removeChild(rowToMove); + this.elTableBody.insertBefore(rowToMove, moveRowBefore); + this.updateRowFunction(rowToMove, this.itemData[rowIndex]); + this.rowOffset += scrollDown ? 1 : -1; + } + } + + // add/remove the row space that was scrolled away to the top buffer height and last scroll point + // add/remove the row space that was scrolled away to the bottom buffer height + let scrolledSpace = this.rowHeight * numScrollRows; + let newTopHeight = prevTopHeight + scrolledSpace; + let newBottomHeight = prevBottomHeight - scrolledSpace; + this.elTopBuffer.setAttribute("height", newTopHeight); + this.elBottomBuffer.setAttribute("height", newBottomHeight); + this.lastRowShiftScrollTop += scrolledSpace; + + // if scrolling more than the total number of visible rows at once then refresh all row data + if (numScrollRowsAbsolute > this.getNumRows()) { + this.refresh(); + } + }, + + refresh: function() { + // block refreshing before rows are initialized + let numRows = this.getNumRows(); + if (numRows === 0) { + return; + } + + let prevScrollTop = this.elTableScroll.scrollTop; + + // start with all row data cleared and initially set to invisible + this.clear(); + + // if we are at the bottom of the list adjust row offset to make sure all rows stay in view + this.refreshRowOffset(); + + // update all row data and set rows visible until max visible items reached + for (let i = 0; i < numRows; i++) { + let rowIndex = i + this.rowOffset; + if (rowIndex >= this.itemData.length) { + break; + } + let rowElementIndex = i + FIRST_ROW_INDEX; + let elRow = this.elTableBody.childNodes[rowElementIndex]; + let itemData = this.itemData[rowIndex]; + this.updateRowFunction(elRow, itemData); + elRow.style.display = ""; // make sure the row is visible + } + + // update the top and bottom buffer heights to adjust for above changes + this.refreshBuffers(); + + // adjust the last row shift scroll point based on how much the current scroll point changed + let scrollTopDifference = this.elTableScroll.scrollTop - prevScrollTop; + if (scrollTopDifference !== 0) { + this.lastRowShiftScrollTop += scrollTopDifference; + if (this.lastRowShiftScrollTop < 0) { + this.lastRowShiftScrollTop = 0; + } + } + }, + + refreshBuffers: function() { + // top buffer height is the number of hidden rows above the top row + let topHiddenRows = this.rowOffset; + let topBufferHeight = this.rowHeight * topHiddenRows; + this.elTopBuffer.setAttribute("height", topBufferHeight); + + // bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row) + let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset; + let bottomBufferHeight = this.rowHeight * bottomHiddenRows; + if (bottomHiddenRows < 0) { + bottomBufferHeight = 0; + } + this.elBottomBuffer.setAttribute("height", bottomBufferHeight); + }, + + refreshRowOffset: function() { + // make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0 + var numRows = this.getNumRows(); + if (this.rowOffset + numRows > this.itemData.length) { + this.rowOffset = this.itemData.length - numRows; + } + if (this.rowOffset < 0) { + this.rowOffset = 0; + } + }, + + onResize: function() { + var that = this.listView; + that.resize(); + }, + + resize: function() { + if (!this.elTableBody || !this.elTableScroll) { + debugPrint("ListView.resize - no valid table body or table scroll element"); + return; + } + + let prevScrollTop = this.elTableScroll.scrollTop; + + // take up available window space + this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT; + let viewableHeight = parseInt(this.elTableScroll.style.height) + 1; + + // remove all existing row elements and clear row list + for (let i = 0; i < this.getNumRows(); i++) { + let elRow = this.elRows[i]; + this.elTableBody.removeChild(elRow); + } + this.elRows = []; + + // create new row elements inserted between the top and bottom buffers up until the max viewable scroll area + let usedHeight = 0; + while (usedHeight < viewableHeight) { + let newRow = this.createRowFunction(); + this.elTableBody.insertBefore(newRow, this.elBottomBuffer); + this.rowHeight = newRow.offsetHeight; + usedHeight += this.rowHeight; + this.elRows.push(newRow); + } + + // add SCROLL_ROWS extras rows for scrolling buffer purposes + for (let i = 0; i < SCROLL_ROWS; i++) { + let scrollRow = this.createRowFunction(); + this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer); + this.elRows.push(scrollRow); + } + + let ths = this.elTableHeaderRow; + let tds = this.getNumRows() > 0 ? this.elRows[0].childNodes : []; + if (!ths) { + debugPrint("ListView.resize - no valid table header row"); + } else if (tds.length !== ths.length) { + debugPrint("ListView.resize - td list size " + tds.length + " does not match th list size " + ths.length); + } + // update the widths of the header cells to match the body cells (using first body row) + for (let i = 0; i < ths.length; i++) { + ths[i].width = tds[i].offsetWidth; + } + + // restore the scroll point to the same scroll point from before above changes + this.elTableScroll.scrollTop = prevScrollTop; + + this.refresh(); + }, + + initialize: function() { + if (!this.elTableBody || !this.elTableScroll) { + debugPrint("ListView.initialize - no valid table body or table scroll element"); + return; + } + + // delete initial blank row + this.elTableBody.deleteRow(0); + + this.elTopBuffer = document.createElement("tr"); + this.elTableBody.appendChild(this.elTopBuffer); + this.elTopBuffer.setAttribute("height", 0); + + this.elBottomBuffer = document.createElement("tr"); + this.elTableBody.appendChild(this.elBottomBuffer); + this.elBottomBuffer.setAttribute("height", 0); + + this.elTableScroll.listView = this; + this.elTableScroll.onscroll = this.onScroll; + window.listView = this; + window.onresize = this.onResize; + + // initialize all row elements + this.resize(); + } +};