// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Mody; using Doozy.Runtime.UIManager.Events; using UnityEngine; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Local namespace Doozy.Runtime.UIManager.Components { /// /// Toggle component that can control other UIToggles, based on UIToggle. /// [RequireComponent(typeof(RectTransform))] [AddComponentMenu("Doozy/UI/Components/UIToggle Group")] [SelectionBase] public partial class UIToggleGroup : UIToggle { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/UI/Components/UIToggle Group", false, 8)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene("UIToggle Group", false, true); } #endif #region Enums /// Defines all the values a Toggle Group can have public enum Value { /// All of the toggle group's toggles are OFF Off = 0, /// All of the toggle group's toggles are ON On = 1, /// The toggle group contains at least one toggle ON and one toggle OFF MixedValues = 2 } /// Defines how a Toggle Group controls its toggles public enum ControlMode { /// /// Toggle values are not enforced in any way /// Allows for all toggles to be OFF /// Passive = 0, /// /// Only one Toggle can be ON at any given time /// Allows for all toggles to be OFF /// OneToggleOn = 1, /// /// Only one Toggle will to be ON at any given time /// One Toggle will be forced ON at all times /// OneToggleOnEnforced = 2, /// /// At least one Toggle needs to be ON at any given time /// Allows for multiple toggles to be ON /// One Toggle will be forced ON at all times /// AnyToggleOnEnforced = 3, } /// Defines the types of (auto) sorting available for toggle groups public enum SortMode { /// Auto sort is disabled Disabled = 0, /// Auto sort by sibling index (the order toggles appear in the Hierarchy) Hierarchy = 1, /// Auto sort by Toggle's GameObject name GameObjectName = 2, /// Auto sort by Toggle Id Name (ignores category) ToggleName = 3 } #endregion public override bool isOn { get => IsOn; set { if (isLocked) return; bool previousValue = IsOn; IsOn = value; if (inToggleGroup) { toggleGroup.ToggleChangedValue(toggle: this, animateChange: true); return; } ValueChanged(previousValue: previousValue, newValue: value, animateChange: true, triggerValueChanged: true); ToggleValue(); } } [SerializeField] private bool OverrideInteractabilityForToggles; /// Override and control the interactable state for all the connected UIToggles public bool overrideInteractabilityForToggles { get => OverrideInteractabilityForToggles; set => OverrideInteractabilityForToggles = value; } [SerializeField] private Value ToggleGroupValue; /// /// Toggle group value /// Off - all of the toggle group's toggles are OFF /// On - all of the toggle group's toggles are ON /// Mixed Values - the toggle group contains at least one toggle ON and one toggle OFF /// public Value toggleGroupValue { get => ToggleGroupValue; private set => ToggleGroupValue = value; } [SerializeField] private ControlMode Mode; /// /// Toggle group's control mode for its toggles /// Passive - toggle values are not enforced in any way (allows for all toggles to be OFF) /// OneToggleOn - only one Toggle can be ON at any given time (allows for all toggles to be OFF) /// OneToggleOnEnforced - only one Toggle can be ON at any given time (one Toggle will be forced ON at all times) /// AnyToggleOnEnforced - at least one Toggle needs to be ON at any given time (one Toggle will be forced ON at all times) /// public ControlMode mode { get => Mode; set { Mode = value; UpdateGroupValue(false); } } [SerializeField] private bool HasMixedValues; /// Marks a toggle group as having toggles of different values public bool hasMixedValues { get => HasMixedValues; private set { if (HasMixedValues == value) return; HasMixedValues = value; if (HasMixedValues) OnToggleGroupMixedValuesCallback?.Execute(); // ValueChanged(previousValue: isOn, newValue: isOn, animateChange: true); } } [SerializeField] private SortMode AutoSort = SortMode.Hierarchy; /// Sort mode used by the AutoSortToggles method /// Disabled - auto sort is disabled /// Hierarchy - auto sort by sibling index (the order toggles appear in the Hierarchy) /// GameObjectName - auto sort by Toggle's GameObject name /// ToggleName - auto sort by Toggle's Id Name (ignores category) /// public SortMode autoSort { get => AutoSort; set { AutoSort = value; SortToggles(value); } } /// Toggle group has mixed values - executed when hasMixedValues becomes TRUE public ModyEvent OnToggleGroupMixedValuesCallback; /// Called when a toggle is added to the toggle group public UIToggleEvent OnToggleAddedCallback = new UIToggleEvent(); /// Called when a toggle is removed from the toggle group public UIToggleEvent OnToggleRemovedCallback = new UIToggleEvent(); /// Triggered when a toggle in the group is triggered (this includes the toggle group itself) public UIToggleEvent OnToggleTriggeredCallback = new UIToggleEvent(); /// The first toggle that will be automatically turned ON OnEnable public UIToggle FirstToggle; /// List of all the toggles controlled by this toggle group public List toggles { get; private set; } = new List(); /// Number of toggles controlled by this toggle group public int numberOfToggles => toggles?.Count ?? 0; /// Number of toggles that are ON public int numberOfTogglesOn => toggles?.Count(toggle => toggle.isOn) ?? 0; /// Number of toggles that are OFF public int numberOfTogglesOff => toggles?.Count(toggle => !toggle.isOn) ?? 0; /// Returns TRUE if at least one toggle is ON public bool anyOfTogglesOn => toggles?.Any(toggle => toggle.isOn) ?? false; /// Returns TRUE if at least one toggle is OFF public bool anyOfTogglesOff => toggles?.Any(toggle => !toggle.isOn) ?? false; /// Returns TRUE if all toggle are ON public bool allTogglesAreOn => toggles?.All(toggle => toggle.isOn) ?? false; /// Returns TRUE if all toggle are OFF public bool allTogglesAreOff => toggles?.All(toggle => !toggle.isOn) ?? false; /// Get all the toggles that are ON public IEnumerable togglesOn => toggles?.Where(toggle => toggle.isOn); /// Get all the toggles that are OFF public IEnumerable togglesOff => toggles?.Where(toggle => !toggle.isOn); /// Get the first toggle that is ON (returns null if no toggles are ON) public UIToggle firstToggleOn => toggles?.FirstOrDefault(toggle => toggle.isOn); /// Get the first toggle that is OFF (returns null if no toggles are OFF) public UIToggle firstToggleOff => toggles?.FirstOrDefault(toggle => !toggle.isOn); /// Get the last toggle that is ON (returns null if no toggles are ON) public UIToggle lastToggleOn => toggles?.LastOrDefault(toggle => toggle.isOn); /// Get the last toggle that is OFF (returns null if no toggles are OFF) public UIToggle lastToggleOff => toggles?.LastOrDefault(toggle => !toggle.isOn); /// Get the index for the first toggle that is ON (returns -1 if no toggles are ON) public int firstToggleOnIndex { get { CleanToggles(); UIToggle firstOn = firstToggleOn; return firstOn == null ? -1 : toggles.IndexOf(firstOn); } } /// Get the index for the first toggle that is OFF (returns -1 if no toggles are OFF) public int firstToggleOffIndex { get { CleanToggles(); UIToggle firstOff = firstToggleOff; return firstOff == null ? -1 : toggles.IndexOf(firstOff); } } /// Get the index for the last toggle that is ON (returns -1 if no toggles are ON) public int lastToggleOnIndex { get { CleanToggles(); UIToggle lastOn = lastToggleOn; return lastOn == null ? -1 : toggles.IndexOf(lastOn); } } /// Get the index for the last toggle that is OFF (returns -1 if no toggles are OFF) public int lastToggleOffIndex { get { CleanToggles(); UIToggle lastOff = lastToggleOff; return lastOff == null ? -1 : toggles.IndexOf(lastOff); } } private bool toggleGroupInitialized { get; set; } protected UIToggleGroup() { OnToggleGroupMixedValuesCallback = new ModyEvent(nameof(OnToggleGroupMixedValuesCallback)); } protected override void Awake() { if (!Application.isPlaying) return; toggleGroupInitialized = false; base.Awake(); } protected override void OnEnable() { if (!Application.isPlaying) return; base.OnEnable(); StartCoroutine(RefreshAllTogglesWithDelay()); } private IEnumerator RefreshAllTogglesWithDelay() { yield return null; RefreshAllToggleValues(); toggleGroupInitialized = true; } protected override void OnDisable() { if (!Application.isPlaying) return; base.OnDisable(); toggleGroupInitialized = false; } private void LateUpdate() { if (!toggleGroupInitialized) return; if (!overrideInteractabilityForToggles) return; foreach (UIToggle toggle in toggles) toggle.interactable = interactable; } protected override void InitializeToggle() { if (toggleInitialized) return; toggleInitialized = true; AddToToggleGroup(toggleGroup); if (inToggleGroup) return; ValueChanged(isOn, isOn, false, false); } /// Clean the toggles list by removing any null references and duplicates public UIToggleGroup CleanToggles() { toggles = toggles .Where(toggle => toggle != null) .Distinct() .ToList(); return this; } /// Automatically sort the toggles by sortMode public void AutoSortToggles() => SortToggles(autoSort); /// Sort the toggles by the given sort mode /// Toggle sort mode public void SortToggles(SortMode toggleSortMode) { CleanToggles(); switch (toggleSortMode) { case SortMode.Disabled: return; case SortMode.Hierarchy: toggles = toggles.OrderBy(t => t.rectTransform.GetSiblingIndex()).ToList(); break; case SortMode.GameObjectName: toggles = toggles.OrderBy(t => t.gameObject.name).ToList(); break; case SortMode.ToggleName: toggles = toggles.OrderBy(t => t.Id.Name).ToList(); break; default: throw new ArgumentOutOfRangeException(nameof(toggleSortMode), toggleSortMode, null); } } /// Add a toggle to this toggle group /// Target toggle public void AddToggle(UIToggle toggle) { if (toggle == null) return; if (toggle == this) return; if (toggles.Contains(toggle)) return; toggles.Add(toggle); toggle.toggleGroup = this; OnToggleAddedCallback?.Invoke(toggle); if (!toggleGroupInitialized) return; AutoSortToggles(); UpdateGroupValue(true); } /// Remove a toggle from this toggle group /// Target toggle public void RemoveToggle(UIToggle toggle) { CleanToggles(); if (toggle == null) return; if (!toggles.Contains(toggle)) return; toggles.Remove(toggle); toggle.toggleGroup = null; OnToggleRemovedCallback?.Invoke(toggle); UpdateGroupValue(true); } /// Notify this toggle group that the given toggle has changed its value /// Toggle that changed its value /// Should the change be animated /// Should the value changed event be triggered public void ToggleChangedValue(UIToggle toggle, bool animateChange = false, bool triggerValueChanged = true) { if (toggle == null) return; if (!toggles.Contains(toggle)) { toggle.RemoveFromToggleGroup(); return; } switch (mode) { case ControlMode.Passive: { toggle.UpdateValueFromGroup(toggle.isOn, animateChange, triggerValueChanged); break; } case ControlMode.OneToggleOn: { if (toggle.isOn && numberOfTogglesOn > 1) { foreach (UIToggle t in toggles.Where(t => t != toggle && t.isOn)) { t.UpdateValueFromGroup(newValue: false, animateChange, triggerValueChanged); } } toggle.UpdateValueFromGroup(toggle.isOn, animateChange, triggerValueChanged); break; } case ControlMode.OneToggleOnEnforced: { if (allTogglesAreOff) { toggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); } else if (toggle.isOn & numberOfTogglesOn > 1) { foreach (UIToggle t in toggles.Where(t => t != toggle && t.isOn)) { t.UpdateValueFromGroup(newValue: false, animateChange, triggerValueChanged); } toggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); } else { toggle.UpdateValueFromGroup(toggle.isOn, animateChange, triggerValueChanged); } break; } case ControlMode.AnyToggleOnEnforced: { if (!toggle.isOn & allTogglesAreOff) { toggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); } else { toggle.UpdateValueFromGroup(toggle.isOn, animateChange, triggerValueChanged); } break; } default: throw new ArgumentOutOfRangeException(); } UpdateGroupValue(animateChange); OnToggleTriggeredCallback?.Invoke(toggle); } protected internal override void UpdateValueFromGroup(bool newValue, bool animateChange, bool triggerValueChanged = true) { switch (mode) { case ControlMode.Passive: if (newValue) { foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); break; } foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(false, animateChange, triggerValueChanged); break; case ControlMode.OneToggleOn: if (newValue) { foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(false, animateChange, triggerValueChanged); break; } toggles[0].UpdateValueFromGroup(true, animateChange, triggerValueChanged); break; case ControlMode.OneToggleOnEnforced: break; case ControlMode.AnyToggleOnEnforced: if (newValue) { foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); break; } UIToggle firstToggle = toggles[0]; firstToggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); foreach (UIToggle toggle in toggles.Where(t => t != firstToggle)) toggle.UpdateValueFromGroup(false, animateChange, triggerValueChanged); break; default: throw new ArgumentOutOfRangeException(); } UpdateGroupValue(animateChange); } public void RefreshAllToggleValues(bool animateChange = true, bool triggerValueChanged = true) { AutoSortToggles(); if (toggles.Count == 0) return; bool setFirstToggleOn; switch (mode) { case ControlMode.Passive: setFirstToggleOn = false; foreach (UIToggle t in toggles) t.UpdateValueFromGroup(t.isOn, false, triggerValueChanged); break; case ControlMode.OneToggleOn: setFirstToggleOn = numberOfTogglesOn > 1; if (!setFirstToggleOn) { foreach (UIToggle t in toggles) t.UpdateValueFromGroup(t.isOn, false, triggerValueChanged); } break; case ControlMode.OneToggleOnEnforced: setFirstToggleOn = numberOfTogglesOn == 0; if (numberOfTogglesOn > 1) { bool foundOneToggleOn = false; foreach (UIToggle t in toggles.Where(t => t.isOn)) { if (!foundOneToggleOn) { foundOneToggleOn = true; continue; } t.UpdateValueFromGroup(false, false, triggerValueChanged); } } break; case ControlMode.AnyToggleOnEnforced: setFirstToggleOn = numberOfTogglesOn == 0; break; default: throw new ArgumentOutOfRangeException(); } if (setFirstToggleOn) { UIToggle firstToggle = GetFirstToggle(); foreach (UIToggle t in toggles.Where(t => t != firstToggle)) t.UpdateValueFromGroup(false, animateChange, triggerValueChanged); if (firstToggle != null) firstToggle.UpdateValueFromGroup(true, animateChange, triggerValueChanged); } UpdateGroupValue(animateChange); } public UIToggle GetFirstToggle() => FirstToggle != null && toggles.Contains(FirstToggle) ? FirstToggle : toggles.Count == 0 ? null : toggles[0]; /// Set all toggles off /// TRUE to animate the change, FALSE to set the value instantly /// TRUE to trigger the value changed event private void SetAllTogglesOff(bool animateChange = false, bool triggerValueChanged = true) { foreach (UIToggle t in toggles) t.UpdateValueFromGroup(false, animateChange, triggerValueChanged); } /// Toggle the toggle's value protected override void ToggleValue() { if (!IsActive() || !IsInteractable()) return; const bool animateChange = true; switch (mode) { case ControlMode.Passive: switch (toggleGroupValue) { case Value.Off: case Value.MixedValues: foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(true, animateChange); break; case Value.On: foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(false, animateChange); break; default: throw new ArgumentOutOfRangeException(); } break; case ControlMode.OneToggleOn: switch (toggleGroupValue) { case Value.Off: toggles[0].UpdateValueFromGroup(true, animateChange); break; case Value.On: case Value.MixedValues: foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(false, animateChange); break; default: throw new ArgumentOutOfRangeException(); } break; case ControlMode.OneToggleOnEnforced: break; case ControlMode.AnyToggleOnEnforced: switch (toggleGroupValue) { case Value.On: UIToggle firstToggle = toggles[0]; firstToggle.UpdateValueFromGroup(true, animateChange); foreach (UIToggle toggle in toggles.Where(item => item != firstToggle)) toggle.UpdateValueFromGroup(false, animateChange); break; case Value.Off: case Value.MixedValues: foreach (UIToggle toggle in toggles) toggle.UpdateValueFromGroup(true, animateChange); break; default: throw new ArgumentOutOfRangeException(); } break; default: throw new ArgumentOutOfRangeException(); } UpdateGroupValue(animateChange); behaviours.GetBehaviour(UIBehaviour.Name.PointerClick)?.Execute(); OnToggleTriggeredCallback?.Invoke(this); } /// Update all the toggles in the toggle group /// TRUE to animate the change, FALSE to set the value instantly /// TRUE to trigger the value changed event public void UpdateGroupValue(bool animateChange, bool triggerValueChanged = true) { if (toggles.Count == 0) return; if (allTogglesAreOn) { toggleGroupValue = Value.On; } else if (allTogglesAreOff) { toggleGroupValue = Value.Off; } else { toggleGroupValue = Value.MixedValues; } hasMixedValues = toggleGroupValue == Value.MixedValues; bool previousValue = isOn; bool newValue = anyOfTogglesOn; if (previousValue != newValue) { this.SetIsOn(newValue, animateChange, triggerValueChanged); } // ValueChanged(previousValue, newValue, animateChange, true); } } }