// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; namespace osu.Game.Screens.Select.Carousel { /// <summary> /// A group which ensures at least one item is selected (if the group itself is selected). /// </summary> public class CarouselGroupEagerSelect : CarouselGroup { public CarouselGroupEagerSelect() { State.ValueChanged += state => { if (state.NewValue == CarouselItemState.Selected) attemptSelection(); }; } /// <summary> /// The last selected item. /// </summary> protected CarouselItem? LastSelected { get; private set; } /// <summary> /// We need to keep track of the index for cases where the selection is removed but we want to select a new item based on its old location. /// </summary> private int lastSelectedIndex; /// <summary> /// To avoid overhead during filter operations, we don't attempt any selections until after all /// items have been filtered. This bool will be true during the base <see cref="Filter(FilterCriteria)"/> /// operation. /// </summary> protected bool DisableSelection; public override void Filter(FilterCriteria criteria) { DisableSelection = true; base.Filter(criteria); DisableSelection = false; attemptSelection(); } public override void RemoveItem(CarouselItem i) { base.RemoveItem(i); if (i != LastSelected) updateSelectedIndex(); } private bool addingItems; public void AddItems(IEnumerable<CarouselItem> items) { addingItems = true; foreach (var i in items) AddItem(i); addingItems = false; attemptSelection(); } public override void AddItem(CarouselItem i) { base.AddItem(i); if (!addingItems) attemptSelection(); } protected override void ChildItemStateChanged(CarouselItem item, CarouselItemState value) { base.ChildItemStateChanged(item, value); switch (value) { case CarouselItemState.Selected: updateSelected(item); break; case CarouselItemState.NotSelected: case CarouselItemState.Collapsed: attemptSelection(); break; } } private void attemptSelection() { if (DisableSelection) return; // we only perform eager selection if we are a currently selected group. if (State.Value != CarouselItemState.Selected) return; // we only perform eager selection if none of our items are in a selected state already. if (Items.Any(i => i.State.Value == CarouselItemState.Selected)) return; PerformSelection(); } /// <summary> /// Finds the item this group would select next if it attempted selection /// </summary> /// <returns>An unfiltered item nearest to the last selected one or null if all items are filtered</returns> public virtual CarouselItem? GetNextToSelect() { if (Items.Count == 0) return null; int forwardsIndex = lastSelectedIndex; int backwardsIndex = Math.Min(lastSelectedIndex, Items.Count - 1); while (true) { bool hasBackwards = backwardsIndex >= 0 && backwardsIndex < Items.Count; bool hasForwards = forwardsIndex < Items.Count; if (!hasBackwards && !hasForwards) return null; if (hasForwards && !Items[forwardsIndex].Filtered.Value) return Items[forwardsIndex]; if (hasBackwards && !Items[backwardsIndex].Filtered.Value) return Items[backwardsIndex]; forwardsIndex++; backwardsIndex--; } } protected virtual void PerformSelection() { CarouselItem? nextToSelect = GetNextToSelect(); if (nextToSelect != null) nextToSelect.State.Value = CarouselItemState.Selected; else updateSelected(null); } private void updateSelected(CarouselItem? newSelection) { if (newSelection != null) LastSelected = newSelection; updateSelectedIndex(); } private void updateSelectedIndex() => lastSelectedIndex = LastSelected == null ? 0 : Math.Max(0, GetIndexOfItem(LastSelected)); } }