diff --git a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md index f5b02e2d5..d040d2291 100644 --- a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md +++ b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added * Added `AlphaBlend` tint mode to `TintEffect`. [PR #1131](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1131) +* Allowed `ToggleCollection` to start with all toggles in an off state (`CurrentIndex = -1`) when `AllowSwitchOff` is true, and updated `CurrentIndex` to `-1` when an active toggle is switched off at runtime. [PR #1146](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1146) ### Fixed diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/ToggleCollectionTests.cs b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/ToggleCollectionTests.cs index d5b6ec8b8..6fab859b3 100644 --- a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/ToggleCollectionTests.cs +++ b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/ToggleCollectionTests.cs @@ -148,6 +148,124 @@ public IEnumerator TestAllowSwitchOff() testInteractor.EndManualInteraction(); Assert.IsFalse(toggleCollection.Toggles[5].IsToggled, "Interactable didn't get detoggled! ToggleCollection should have allowed it."); + Assert.AreEqual(-1, toggleCollection.CurrentIndex, "ToggleCollection should update CurrentIndex to -1 when active toggle is detoggled!"); + } + + /// + /// Ensures ToggleCollection can start with all toggles switched off when AllowSwitchOff is true. + /// + [UnityTest] + public IEnumerator TestStartSwitchedOff() + { + var parent = new GameObject(); + + for (int i = 0; i < 5; i++) + { + var toggleObject = new GameObject(); + toggleObject.transform.parent = parent.transform; + var interactable = toggleObject.AddComponent(); + interactable.ToggleMode = StatefulInteractable.ToggleType.Toggle; + } + + var toggleCollection = parent.AddComponent(); + toggleCollection.AllowSwitchOff = true; + toggleCollection.CurrentIndex = -1; + yield return null; + + Assert.AreEqual(5, toggleCollection.Toggles.Count, "ToggleCollection didn't detect all of the toggles!"); + Assert.AreEqual(-1, toggleCollection.CurrentIndex, "ToggleCollection should remain at CurrentIndex = -1 when starting switched off!"); + foreach (var toggle in toggleCollection.Toggles) + { + Assert.IsFalse(toggle.IsToggled, "No toggle should be toggled when starting switched off!"); + } + } + + /// + /// Ensures setting CurrentIndex to -1 deselects all toggles when AllowSwitchOff is true, + /// but is rejected when AllowSwitchOff is false. + /// + [UnityTest] + public IEnumerator TestSetSelectionMinusOne() + { + var parent = new GameObject(); + + for (int i = 0; i < 5; i++) + { + var toggleObject = new GameObject(); + toggleObject.transform.parent = parent.transform; + var interactable = toggleObject.AddComponent(); + interactable.ToggleMode = StatefulInteractable.ToggleType.Toggle; + } + + var toggleCollection = parent.AddComponent(); + toggleCollection.AllowSwitchOff = true; + yield return null; + + // Select toggle 2 + toggleCollection.CurrentIndex = 2; + Assert.AreEqual(2, toggleCollection.CurrentIndex); + Assert.IsTrue(toggleCollection.Toggles[2].IsToggled); + + // Set selection to -1 to deselect all + toggleCollection.CurrentIndex = -1; + Assert.AreEqual(-1, toggleCollection.CurrentIndex, "CurrentIndex should be -1 after setting selection to -1 with AllowSwitchOff = true!"); + foreach (var toggle in toggleCollection.Toggles) + { + Assert.IsFalse(toggle.IsToggled, "All toggles should be untoggled after setting CurrentIndex to -1!"); + } + + // When AllowSwitchOff is false, setting CurrentIndex to -1 should be rejected + toggleCollection.AllowSwitchOff = false; + toggleCollection.CurrentIndex = 3; + Assert.AreEqual(3, toggleCollection.CurrentIndex); + + LogAssert.Expect(LogType.Warning, "Index out of range of ToggleCollection: -1"); + toggleCollection.CurrentIndex = -1; + Assert.AreEqual(3, toggleCollection.CurrentIndex, "CurrentIndex should remain unchanged when AllowSwitchOff is false!"); + } + + /// + /// Ensures OnToggleSelected fires with -1 when an active toggle is detoggled. + /// + [UnityTest] + public IEnumerator TestSwitchOffEvent() + { + var parent = new GameObject(); + + for (int i = 0; i < 5; i++) + { + var toggleObject = new GameObject(); + toggleObject.transform.parent = parent.transform; + var interactable = toggleObject.AddComponent(); + interactable.ToggleMode = StatefulInteractable.ToggleType.Toggle; + } + + var toggleCollection = parent.AddComponent(); + toggleCollection.AllowSwitchOff = true; + yield return null; + + int lastSelected = -999; + toggleCollection.OnToggleSelected.AddListener((index) => lastSelected = index); + + var testInteractor = parent.AddComponent(); + + // Toggle on toggle 2 + testInteractor.StartManualInteraction(toggleCollection.Toggles[2] as IXRSelectInteractable); + yield return null; + yield return null; + testInteractor.EndManualInteraction(); + + Assert.AreEqual(2, lastSelected, "OnToggleSelected should have fired with index 2!"); + Assert.AreEqual(2, toggleCollection.CurrentIndex); + + // Toggle off toggle 2 + testInteractor.StartManualInteraction(toggleCollection.Toggles[2] as IXRSelectInteractable); + yield return null; + yield return null; + testInteractor.EndManualInteraction(); + + Assert.AreEqual(-1, lastSelected, "OnToggleSelected should have fired with index -1 when detoggled!"); + Assert.AreEqual(-1, toggleCollection.CurrentIndex); } } } diff --git a/org.mixedrealitytoolkit.uxcore/Toggle/ToggleCollection.cs b/org.mixedrealitytoolkit.uxcore/Toggle/ToggleCollection.cs index bc4cbfdf2..d3ff26d1d 100644 --- a/org.mixedrealitytoolkit.uxcore/Toggle/ToggleCollection.cs +++ b/org.mixedrealitytoolkit.uxcore/Toggle/ToggleCollection.cs @@ -43,7 +43,7 @@ public List Toggles // Add listeners to new list AddSelectionListeners(); - int index = Mathf.Clamp(CurrentIndex, 0, toggles.Count - 1); + int index = Mathf.Clamp(CurrentIndex, allowSwitchOff ? -1 : 0, toggles.Count - 1); SetSelection(index); } } @@ -71,7 +71,7 @@ public bool AllowSwitchOff } [SerializeField] - [Tooltip("Currently selected index in the ToggleCollection, default is 0")] + [Tooltip("Currently selected index in the ToggleCollection, default is 0. Set to -1 to start with no toggle selected (requires AllowSwitchOff).")] private int currentIndex; /// @@ -83,12 +83,12 @@ public int CurrentIndex set => SetSelection(value); } - [Tooltip("This event is triggered when any of the toggles in the ToggleCollection are selected. The event data is the index of the toggle button with in the ToggleCollection.")] + [Tooltip("This event is triggered when any of the toggles in the ToggleCollection are selected, or when all toggles are deselected (-1). The event data is the index of the toggle button within the ToggleCollection.")] [SerializeField] private ToggleSelectedEvent onToggleSelected = new ToggleSelectedEvent(); /// - /// This event is triggered when any of the toggles in the are selected. The event data is the index of the toggle button within the . + /// This event is triggered when any of the toggles in the are selected, or when all toggles are deselected (-1). The event data is the index of the toggle button within the . /// public ToggleSelectedEvent OnToggleSelected { @@ -97,6 +97,7 @@ public ToggleSelectedEvent OnToggleSelected // List of the actions for the toggles in ToggleCollection. private List> toggleActions = new List>(); + private List> toggleDeselectionActions = new List>(); /// /// A Unity Editor only event function that is called when the script is loaded or a value changes in the Unity Inspector. @@ -143,6 +144,10 @@ private void Start() SetSelection(CurrentIndex, true); Toggles[CurrentIndex].ForceSetToggled(true); } + else if (CurrentIndex == -1 && allowSwitchOff) + { + SetSelection(-1, true); + } } // Initialize the interactables with the proper allow-toggle-off setting. @@ -156,7 +161,7 @@ private void Start() /// public void SetSelection(int index, bool force = false) { - if (index < 0 || Toggles.Count <= index || Toggles == null || !isActiveAndEnabled) + if (index < (allowSwitchOff ? -1 : 0) || Toggles.Count <= index || Toggles == null || !isActiveAndEnabled) { Debug.LogWarning("Index out of range of ToggleCollection: " + index, this); return; @@ -202,10 +207,19 @@ private void AddSelectionListeners() int itemIndex = i; UnityAction setSelectionAction = (_) => SetSelection(itemIndex); + UnityAction setDeselectionAction = (_) => + { + if (allowSwitchOff && CurrentIndex == itemIndex) + { + SetSelection(-1); + } + }; toggleActions.Add(setSelectionAction); + toggleDeselectionActions.Add(setDeselectionAction); Toggles[i].IsToggled.OnEntered.AddListener(setSelectionAction); + Toggles[i].IsToggled.OnExited.AddListener(setDeselectionAction); Toggles[i].ToggleMode = allowSwitchOff ? StatefulInteractable.ToggleType.Toggle : StatefulInteractable.ToggleType.OneWayToggle; } } @@ -217,9 +231,11 @@ private void RemoveSelectionListeners() if (Toggles[i] == null) { continue; } Toggles[i].IsToggled.OnEntered.RemoveListener(toggleActions[i]); + Toggles[i].IsToggled.OnExited.RemoveListener(toggleDeselectionActions[i]); } toggleActions.Clear(); + toggleDeselectionActions.Clear(); } ///