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();
}
///