diff --git a/interface/resources/controllers/hydra.json b/interface/resources/controllers/hydra.json index 066676140c..716d283a86 100644 --- a/interface/resources/controllers/hydra.json +++ b/interface/resources/controllers/hydra.json @@ -3,9 +3,17 @@ "channels": [ { "from": "Hydra.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "Hydra.LX", "to": "Standard.LX" }, + { "from": "Hydra.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "Hydra.LT", "to": "Standard.LT" }, { "from": "Hydra.RY", "filters": "invert", "to": "Standard.RY" }, { "from": "Hydra.RX", "to": "Standard.RX" }, + { "from": "Hydra.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "Hydra.RT", "to": "Standard.RT" }, { "from": "Hydra.LB", "to": "Standard.LB" }, diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 3def439221..001d5b8716 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -8,6 +8,10 @@ { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "OculusTouch.LX", "to": "Standard.LX" }, + { "from": "OculusTouch.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, { "from": "OculusTouch.LeftGrip", "to": "Standard.LeftGrip" }, @@ -15,6 +19,10 @@ { "from": "OculusTouch.RY", "filters": "invert", "to": "Standard.RY" }, { "from": "OculusTouch.RX", "to": "Standard.RX" }, + { "from": "OculusTouch.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, { "from": "OculusTouch.RightGrip", "to": "Standard.RightGrip" }, diff --git a/interface/resources/controllers/standard_navigation.json b/interface/resources/controllers/standard_navigation.json index c9dc90cbe6..81096a7230 100644 --- a/interface/resources/controllers/standard_navigation.json +++ b/interface/resources/controllers/standard_navigation.json @@ -10,14 +10,7 @@ { "from": "Standard.RB", "to": "Actions.UiNavGroup" }, { "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" }, { "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" }, - { - "from": [ "Standard.RT", "Standard.LT" ], - "to": "Actions.UiNavSelect", - "filters": [ - { "type": "deadZone", "min": 0.5 }, - "constrainToInteger" - ] - }, + { "from": [ "Standard.RTClick", "Standard.LTClick" ], "to": "Actions.UiNavSelect" }, { "from": "Standard.LX", "to": "Actions.UiNavLateral", "filters": [ diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 6be672900a..79114b8141 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -9,6 +9,8 @@ { "type": "deadZone", "min": 0.05 } ] }, + { "from": "Vive.LTClick", "to": "Standard.LTClick" }, + { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, @@ -21,6 +23,8 @@ { "type": "deadZone", "min": 0.05 } ] }, + { "from": "Vive.RTClick", "to": "Standard.RTClick" }, + { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/interface/resources/qml/menus/VrMenuView.qml b/interface/resources/qml/menus/VrMenuView.qml index d8cc0e6667..5db13fc160 100644 --- a/interface/resources/qml/menus/VrMenuView.qml +++ b/interface/resources/qml/menus/VrMenuView.qml @@ -45,6 +45,7 @@ FocusScope { onVisibleChanged: recalcSize(); onCountChanged: recalcSize(); focus: true + highlightMoveDuration: 0 highlight: Rectangle { anchors { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0e0f56438e..fb48472b14 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4705,6 +4705,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, RayToOverlayIntersectionResultFromScriptValue); + scriptEngine->registerGlobalObject("OffscreenFlags", DependencyManager::get()->getFlags()); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index a9d317d407..02ae5706b7 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -65,6 +65,8 @@ Input::NamedVector StandardController::getAvailableInputs() const { // Triggers makePair(LT, "LT"), makePair(RT, "RT"), + makePair(LT_CLICK, "LTClick"), + makePair(RT_CLICK, "RTClick"), // Finger abstractions makePair(LEFT_PRIMARY_THUMB, "LeftPrimaryThumb"), diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index 501f97f04b..c21d8a2f6e 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -46,6 +46,7 @@ namespace controller { LS_CENTER, LS_X, LS_Y, + LT_CLICK, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -56,6 +57,7 @@ namespace controller { RS_CENTER, RS_X, RS_Y, + RT_CLICK, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp index 5ae52893e0..7dedfda3cb 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp @@ -26,6 +26,7 @@ #include "filters/InvertFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" +#include "conditionals/AndConditional.h" using namespace controller; @@ -58,12 +59,22 @@ QObject* RouteBuilderProxy::peek(bool enable) { } QObject* RouteBuilderProxy::when(const QScriptValue& expression) { - _route->conditional = _parent.conditionalFor(expression); + auto newConditional = _parent.conditionalFor(expression); + if (_route->conditional) { + _route->conditional = ConditionalPointer(new AndConditional(_route->conditional, newConditional)); + } else { + _route->conditional = newConditional; + } return this; } QObject* RouteBuilderProxy::whenQml(const QJSValue& expression) { - _route->conditional = _parent.conditionalFor(expression); + auto newConditional = _parent.conditionalFor(expression); + if (_route->conditional) { + _route->conditional = ConditionalPointer(new AndConditional(_route->conditional, newConditional)); + } else { + _route->conditional = newConditional; + } return this; } diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 1a7d4b2328..28aba4a365 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -29,6 +29,9 @@ class OffscreenFlags : public QObject { Q_OBJECT Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) + // Allow scripts that are doing their own navigation support to disable navigation focus (i.e. handControllerPointer.js) + Q_PROPERTY(bool navigationFocusDisabled READ isNavigationFocusDisabled WRITE setNavigationFocusDisabled NOTIFY navigationFocusDisabledChanged) + public: OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} @@ -40,11 +43,21 @@ public: } } + bool isNavigationFocusDisabled() const { return _navigationFocusDisabled; } + void setNavigationFocusDisabled(bool disabled) { + if (_navigationFocusDisabled != disabled) { + _navigationFocusDisabled = disabled; + emit navigationFocusDisabledChanged(); + } + } + signals: void navigationFocusedChanged(); + void navigationFocusDisabledChanged(); private: bool _navigationFocused { false }; + bool _navigationFocusDisabled{ false }; }; QString fixupHifiUrl(const QString& urlString) { @@ -103,6 +116,10 @@ bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { OffscreenUi::OffscreenUi() { } +QObject* OffscreenUi::getFlags() { + return offscreenFlags; +} + void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); auto rootContext = getRootContext(); @@ -392,7 +409,7 @@ QVariant OffscreenUi::waitForInputDialogResult(QQuickItem* inputDialog) { } bool OffscreenUi::navigationFocused() { - return offscreenFlags->isNavigationFocused(); + return !offscreenFlags->isNavigationFocusDisabled() && offscreenFlags->isNavigationFocused(); } void OffscreenUi::setNavigationFocused(bool focused) { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index e1d552c978..2bd00bf612 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -55,7 +55,7 @@ public: bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); - + QObject* getFlags(); QQuickItem* getDesktop(); QQuickItem* getToolWindow(); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 4a515978c3..85feebda11 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -22,6 +22,7 @@ #include #include + #include #include @@ -284,7 +285,6 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u vr::VRControllerState_t controllerState = vr::VRControllerState_t(); if (_system->GetControllerState(deviceIndex, &controllerState)) { - // process each button for (uint32_t i = 0; i < vr::k_EButton_Max; ++i) { auto mask = vr::ButtonMaskFromId((vr::EVRButtonId)i); @@ -342,6 +342,11 @@ void ViveControllerManager::InputDevice::handleAxisEvent(float deltaTime, uint32 _axisStateMap[isLeftHand ? LY : RY] = stick.y; } else if (axis == vr::k_EButton_SteamVR_Trigger) { _axisStateMap[isLeftHand ? LT : RT] = x; + // The click feeling on the Vive controller trigger represents a value of *precisely* 1.0, + // so we can expose that as an additional button + if (x >= 1.0f) { + _buttonPressedMap.insert(isLeftHand ? LT_CLICK : RT_CLICK); + } } } @@ -463,10 +468,15 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RS_X, "RSX"), makePair(RS_Y, "RSY"), + // triggers makePair(LT, "LT"), makePair(RT, "RT"), + // Trigger clicks + makePair(LT_CLICK, "LTClick"), + makePair(RT_CLICK, "RTClick"), + // low profile side grip button. makePair(LEFT_GRIP, "LeftGrip"), makePair(RIGHT_GRIP, "RightGrip"), diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index b66fca1448..13d71dca1c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -29,9 +29,8 @@ var WANT_DEBUG_SEARCH_NAME = null; var SPARK_MODEL_SCALE_FACTOR = 0.75; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing -var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab -var TRIGGER_OFF_VALUE = 0.15; +var TRIGGER_OFF_VALUE = 0.1; +var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab var COLLIDE_WITH_AV_AFTER_RELEASE_DELAY = 0.25; // seconds @@ -393,6 +392,7 @@ function MyController(hand) { this.entityActivated = false; this.triggerValue = 0; // rolling average of trigger value + this.triggerClicked = false; this.rawTriggerValue = 0; this.rawSecondaryValue = 0; this.rawThumbValue = 0; @@ -852,6 +852,10 @@ function MyController(hand) { _this.rawTriggerValue = value; }; + this.triggerClick = function (value) { + _this.triggerClicked = value; + }; + this.secondaryPress = function (value) { _this.rawSecondaryValue = value; }; @@ -864,7 +868,7 @@ function MyController(hand) { }; this.triggerSmoothedGrab = function () { - return this.triggerValue > TRIGGER_GRAB_VALUE; + return this.triggerClicked; }; this.triggerSmoothedSqueezed = function () { @@ -2293,7 +2297,10 @@ var MAPPING_NAME = "com.highfidelity.handControllerGrab"; var mapping = Controller.newMapping(MAPPING_NAME); mapping.from([Controller.Standard.RT]).peek().to(rightController.triggerPress); +mapping.from([Controller.Standard.RTClick]).peek().to(rightController.triggerClick); + mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress); +mapping.from([Controller.Standard.LTClick]).peek().to(leftController.triggerClick); mapping.from([Controller.Standard.RB]).peek().to(rightController.secondaryPress); mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index cb35414cf8..9ff82b3767 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -49,23 +49,23 @@ function Trigger(label) { var that = this; that.label = label; that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing - that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab - that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab - that.TRIGGER_OFF_VALUE = 0.15; + that.TRIGGER_OFF_VALUE = 0.10; + that.TRIGGER_ON_VALUE = that.TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab that.rawTriggerValue = 0; that.triggerValue = 0; // rolling average of trigger value - that.triggerPress = function (value) { - that.rawTriggerValue = value; - }; + that.triggerClicked = false; + that.triggerClick = function (value) { that.triggerClicked = value; }; + that.triggerPress = function (value) { that.rawTriggerValue = value; }; that.updateSmoothedTrigger = function () { // e.g., call once/update for effect var triggerValue = that.rawTriggerValue; // smooth out trigger value that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); + OffscreenFlags.navigationFocusDisabled = that.triggerValue != 0.0; }; // Current smoothed state, without hysteresis. Answering booleans. - that.triggerSmoothedGrab = function () { - return that.triggerValue > that.TRIGGER_GRAB_VALUE; + that.triggerSmoothedClick = function () { + return that.triggerClicked; }; that.triggerSmoothedSqueezed = function () { return that.triggerValue > that.TRIGGER_ON_VALUE; @@ -81,7 +81,7 @@ function Trigger(label) { that.updateSmoothedTrigger(); // The first two are independent of previous state: - if (that.triggerSmoothedGrab()) { + if (that.triggerSmoothedClick()) { state = 'full'; } else if (that.triggerSmoothedReleased()) { state = null; @@ -365,6 +365,8 @@ Script.scriptEnding.connect(clickMapping.disable); // Gather the trigger data for smoothing. clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +clickMapping.from(Controller.Standard.RTClick).peek().to(rightTrigger.triggerClick); +clickMapping.from(Controller.Standard.LTClick).peek().to(leftTrigger.triggerClick); // Full smoothed trigger is a click. function isPointingAtOverlayStartedNonFullTrigger(trigger) { // true if isPointingAtOverlay AND we were NOT full triggered when we became so. @@ -493,7 +495,10 @@ function checkSettings() { updateRecommendedArea(); } checkSettings(); + var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL); Script.scriptEnding.connect(function () { Script.clearInterval(settingsChecker); + OffscreenFlags.navigationFocusDisabled = false; }); + diff --git a/scripts/system/libraries/Trigger.js b/scripts/system/libraries/Trigger.js new file mode 100644 index 0000000000..ffde021f5d --- /dev/null +++ b/scripts/system/libraries/Trigger.js @@ -0,0 +1,87 @@ +"use strict"; + +/*jslint vars: true, plusplus: true*/ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/ + +Trigger = function(properties) { + properties = properties || {}; + var that = this; + that.label = properties.label || Math.random(); + that.SMOOTH_RATIO = properties.smooth || 0.1; // Time averaging of trigger - 0.0 disables smoothing + that.DEADZONE = properties.deadzone || 0.10; // Once pressed, a trigger must fall below the deadzone to be considered un-pressed once pressed. + that.HYSTERESIS = properties.hystersis || 0.05; // If not pressed, a trigger must go above DEADZONE + HYSTERSIS to be considered pressed + + that.value = 0; + that.pressed = false; + that.clicked = false; + + // Handlers + that.onPress = properties.onPress || function(){ + print("Pressed trigger " + that.label) + }; + that.onRelease = properties.onRelease || function(){ + print("Released trigger " + that.label) + }; + that.onClick = properties.onClick || function(){ + print("Clicked trigger " + that.label) + }; + that.onUnclick = properties.onUnclick || function(){ + print("Unclicked trigger " + that.label) + }; + + // Getters + that.isPressed = function() { + return that.pressed; + } + + that.isClicked = function() { + return that.clicked; + } + + that.getValue = function() { + return that.value; + } + + + // Private values + var controller = properties.controller || Controller.Standard.LT; + var controllerClick = properties.controllerClick || Controller.Standard.LTClick; + that.mapping = Controller.newMapping('com.highfidelity.controller.trigger.' + controller + '-' + controllerClick + '.' + that.label + Math.random()); + Script.scriptEnding.connect(that.mapping.disable); + + // Setup mapping, + that.mapping.from(controller).peek().to(function(value) { + that.value = (that.value * that.SMOOTH_RATIO) + + (value * (1.0 - that.SMOOTH_RATIO)); + + var oldPressed = that.pressed; + if (!that.pressed && that.value >= (that.DEADZONE + that.HYSTERESIS)) { + that.pressed = true; + that.onPress(); + } + + if (that.pressed && that.value < that.HYSTERESIS) { + that.pressed = false; + that.onRelease(); + } + }); + + that.mapping.from(controllerClick).peek().to(function(value){ + if (!that.clicked && value > 0.0) { + that.clicked = true; + that.onClick(); + } + if (that.clicked && value == 0.0) { + that.clicked = false; + that.onUnclick(); + } + }); + + that.enable = function() { + that.mapping.enable(); + } + + that.disable = function() { + that.mapping.disable(); + } +}