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