// Copyright (c) ppy Pty Ltd . 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.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomsContainer : CompositeDrawable, IKeyBindingHandler { public Action JoinRequested; private readonly IBindableList rooms = new BindableList(); private readonly FillFlowContainer roomFlow; public IReadOnlyList Rooms => roomFlow; [Resolved(CanBeNull = true)] private Bindable filter { get; set; } [Resolved] private Bindable selectedRoom { get; set; } [Resolved] private IRoomManager roomManager { get; set; } [Resolved(CanBeNull = true)] private LoungeSubScreen loungeSubScreen { get; set; } // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public RoomsContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Child = roomFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2), } }; } protected override void LoadComplete() { rooms.CollectionChanged += roomsChanged; roomManager.RoomsUpdated += updateSorting; rooms.BindTo(roomManager.Rooms); filter?.BindValueChanged(criteria => Filter(criteria.NewValue)); selectedRoom.BindValueChanged(selection => { updateSelection(); }, true); } private void updateSelection() => roomFlow.Children.ForEach(r => r.State = r.Room == selectedRoom.Value ? SelectionState.Selected : SelectionState.NotSelected); public void Filter(FilterCriteria criteria) { roomFlow.Children.ForEach(r => { if (criteria == null) r.MatchingFilter = true; else { bool matchingFilter = true; matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); r.MatchingFilter = matchingFilter; } }); } private void roomsChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Add: addRooms(args.NewItems.Cast()); break; case NotifyCollectionChangedAction.Remove: removeRooms(args.OldItems.Cast()); break; } } private void addRooms(IEnumerable rooms) { foreach (var room in rooms) { roomFlow.Add(new DrawableRoom(room) { Action = () => { if (room == selectedRoom.Value) { joinSelected(); return; } selectRoom(room); } }); } Filter(filter?.Value); updateSelection(); } private void removeRooms(IEnumerable rooms) { foreach (var r in rooms) { var toRemove = roomFlow.Single(d => d.Room == r); toRemove.Action = null; roomFlow.Remove(toRemove); selectRoom(null); } } private void updateSorting() { foreach (var room in roomFlow) roomFlow.SetLayoutPosition(room, room.Room.Position.Value); } private void selectRoom(Room room) => selectedRoom.Value = room; private void joinSelected() { if (selectedRoom.Value == null) return; JoinRequested?.Invoke(selectedRoom.Value); } protected override bool OnClick(ClickEvent e) { selectRoom(null); return base.OnClick(e); } #region Key selection logic (shared with BeatmapCarousel) public bool OnPressed(GlobalAction action) { switch (action) { case GlobalAction.Select: joinSelected(); return true; case GlobalAction.SelectNext: beginRepeatSelection(() => selectNext(1), action); return true; case GlobalAction.SelectPrevious: beginRepeatSelection(() => selectNext(-1), action); return true; } return false; } public void OnReleased(GlobalAction action) { switch (action) { case GlobalAction.SelectNext: case GlobalAction.SelectPrevious: endRepeatSelection(action); break; } } private ScheduledDelegate repeatDelegate; private object lastRepeatSource; /// /// Begin repeating the specified selection action. /// /// The action to perform. /// The source of the action. Used in conjunction with to only cancel the correct action (most recently pressed key). private void beginRepeatSelection(Action action, object source) { endRepeatSelection(); lastRepeatSource = source; repeatDelegate = this.BeginKeyRepeat(Scheduler, action); } private void endRepeatSelection(object source = null) { // only the most recent source should be able to cancel the current action. if (source != null && !EqualityComparer.Default.Equals(lastRepeatSource, source)) return; repeatDelegate?.Cancel(); repeatDelegate = null; lastRepeatSource = null; } private void selectNext(int direction) { var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); Room room; if (selectedRoom.Value == null) room = visibleRooms.FirstOrDefault()?.Room; else { if (direction < 0) visibleRooms = visibleRooms.Reverse(); room = visibleRooms.SkipWhile(r => r.Room != selectedRoom.Value).Skip(1).FirstOrDefault()?.Room; } // we already have a valid selection only change selection if we still have a room to switch to. if (room != null) selectRoom(room); } #endregion protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (roomManager != null) roomManager.RoomsUpdated -= updateSorting; } } }