using EventInput; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework.Input; namespace Barotrauma { public class GUIListBox : GUIComponent, IKeyboardSubscriber { protected List selected; public delegate bool OnSelectedHandler(GUIComponent component, object obj); public OnSelectedHandler OnSelected; public delegate object CheckSelectedHandler(); public CheckSelectedHandler CheckSelected; public GUIScrollBar ScrollBar { get; private set; } public GUIFrame Content { get; private set; } private int totalSize; private int spacing; private bool childrenNeedsRecalculation; private bool scrollBarNeedsRecalculation; public bool SelectMultiple; public bool HideChildrenOutsideFrame = true; private bool useGridLayout; public bool UseGridLayout { get { return useGridLayout; } set { if (useGridLayout == value) return; useGridLayout = value; childrenNeedsRecalculation = true; scrollBarNeedsRecalculation = true; } } public GUIComponent SelectedComponent { get { return selected.FirstOrDefault(); } } public bool Selected { get; set; } public List AllSelected { get { return selected; } } public object SelectedData { get { return SelectedComponent?.UserData; } } public int SelectedIndex { get { if (SelectedComponent == null) return -1; return Content.RectTransform.GetChildIndex(SelectedComponent.RectTransform); } } public float BarScroll { get { return ScrollBar.BarScroll; } set { ScrollBar.BarScroll = value; } } public float BarSize { get { return ScrollBar.BarSize; } } public float TotalSize { get { return totalSize; } } public int Spacing { get { return spacing; } set { spacing = value; } } public override Color Color { get { return base.Color; } set { base.Color = value; Content.Color = value; } } /// /// Disables the scroll bar without hiding it. /// public bool ScrollBarEnabled { get; set; } = true; public bool ScrollBarVisible { get { return ScrollBar.Visible; } set { ScrollBar.Visible = value; AutoHideScrollBar = false; } } /// /// Automatically hides the scroll bar when the content fits in. /// public bool AutoHideScrollBar { get; set; } = true; public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "") : base(style, rectT) { selected = new List(); Point frameSize = isHorizontal ? new Point(rectT.NonScaledSize.X, rectT.NonScaledSize.Y - 20) : new Point(rectT.NonScaledSize.X - 20, rectT.NonScaledSize.Y); Content = new GUIFrame(new RectTransform(frameSize, rectT), style) { CanBeFocused = false }; Content.RectTransform.ChildrenChanged += (_) => { scrollBarNeedsRecalculation = true; childrenNeedsRecalculation = true; }; if (style != null) GUI.Style.Apply(Content, "", this); if (color.HasValue) { this.color = color.Value; } if (isHorizontal) { ScrollBar = new GUIScrollBar(new RectTransform(new Point(Rect.Width, 20), rectT, Anchor.BottomLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(0, 20) }, isHorizontal: isHorizontal); } else { ScrollBar = new GUIScrollBar(new RectTransform(new Point(20, Rect.Height), rectT, Anchor.TopRight, Pivot.TopLeft) { AbsoluteOffset = new Point(20, 0) }, isHorizontal: isHorizontal); } UpdateScrollBarSize(); Enabled = true; ScrollBar.BarScroll = 0.0f; RectTransform.ScaleChanged += UpdateDimensions; RectTransform.SizeChanged += UpdateDimensions; } private void UpdateDimensions() { if (!ScrollBarEnabled) { Content.RectTransform.NonScaledSize = Rect.Size; } else { Point frameSize = ScrollBar.IsHorizontal ? new Point(Rect.Width, Rect.Height - 20) : new Point(Rect.Width - 20, Rect.Height); Content.RectTransform.NonScaledSize = frameSize; } ScrollBar.RectTransform.NonScaledSize = ScrollBar.IsHorizontal ? new Point(Rect.Width, 20) : new Point(20, Rect.Height); } public void Select(object userData, bool force = false, bool autoScroll = true) { var children = Content.Children; int i = 0; foreach (GUIComponent child in children) { if ((child.UserData != null && child.UserData.Equals(userData)) || (child.UserData == null && userData == null)) { Select(i, force, autoScroll); if (!SelectMultiple) return; } i++; } } private void RepositionChildren() { var children = Content.Children; int x = 0, y = 0; if (ScrollBar.BarSize < 1.0f) { if (ScrollBar.IsHorizontal) { x -= (int)((totalSize - Content.Rect.Width) * ScrollBar.BarScroll); } else { y -= (int)((totalSize - Content.Rect.Height) * ScrollBar.BarScroll); } } for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); if (!child.Visible) { continue; } if (RectTransform != null) { if (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y) { child.RectTransform.AbsoluteOffset = new Point(x, y); } } if (useGridLayout) { if (ScrollBar.IsHorizontal) { if (y + child.Rect.Height + spacing > Content.Rect.Height) { y = 0; x += child.Rect.Width + spacing; if (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y) { child.RectTransform.AbsoluteOffset = new Point(x, y); } y += child.Rect.Height + spacing; } else { y += child.Rect.Height + spacing; } } else { if (x + child.Rect.Width + spacing > Content.Rect.Width) { x = 0; y += child.Rect.Height + spacing; if (child.RectTransform.AbsoluteOffset.X != x || child.RectTransform.AbsoluteOffset.Y != y) { child.RectTransform.AbsoluteOffset = new Point(x, y); } x += child.Rect.Width + spacing; } else { x += child.Rect.Width + spacing; } } } else { if (ScrollBar.IsHorizontal) { x += child.Rect.Width + spacing; } else { y += child.Rect.Height + spacing; } } } } private void UpdateChildrenRect() { for (int i = 0; i < Content.CountChildren; i++) { var child = Content.RectTransform.GetChild(i)?.GUIComponent; if (child == null) continue; // selecting if (Enabled && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) { child.State = ComponentState.Hover; if (PlayerInput.LeftButtonClicked()) { Select(i, autoScroll: false); } } else if (selected.Contains(child)) { child.State = ComponentState.Selected; if (CheckSelected != null) { if (CheckSelected() != child.UserData) selected.Remove(child); } } else { child.State = ComponentState.None; } } } public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { if (!Visible) { return; } if (childrenNeedsRecalculation) { foreach (GUIComponent child in Content.Children) { ClampChildMouseRects(child); } RepositionChildren(); childrenNeedsRecalculation = false; } UpdateOrder = order; GUI.AddToUpdateList(this); if (ignoreChildren) { OnAddedToGUIUpdateList?.Invoke(this); return; } Content.AddToGUIUpdateList(true, order); int lastVisible = 0; for (int i = 0; i < Content.CountChildren; i++) { var child = Content.GetChild(i); if (!child.Visible) continue; if (!IsChildInsideFrame(child)) { if (lastVisible > 0) break; continue; } lastVisible = i; child.AddToGUIUpdateList(false, order); } if (ScrollBar.Enabled) { ScrollBar.AddToGUIUpdateList(false, order); } OnAddedToGUIUpdateList?.Invoke(this); } private void ClampChildMouseRects(GUIComponent child) { child.ClampMouseRectToParent = true; //no need to go through grandchildren if the child is a GUIListBox, it handles this by itself if (child is GUIListBox) return; foreach (GUIComponent grandChild in child.Children) { ClampChildMouseRects(grandChild); } } protected override void Update(float deltaTime) { if (!Visible) return; UpdateChildrenRect(); RepositionChildren(); if (scrollBarNeedsRecalculation) { UpdateScrollBarSize(); scrollBarNeedsRecalculation = false; } ScrollBar.Enabled = ScrollBarEnabled && ScrollBar.BarSize < 1.0f; if (AutoHideScrollBar) { ScrollBar.Visible = ScrollBar.BarSize < 1.0f; } if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && PlayerInput.ScrollWheelSpeed != 0) { ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; } } public void SelectNext(bool force = false, bool autoScroll = true) { Select(Math.Min(Content.CountChildren - 1, SelectedIndex + 1), force, autoScroll); } public void SelectPrevious(bool force = false, bool autoScroll = true) { Select(Math.Max(0, SelectedIndex - 1), force, autoScroll); } public void Select(int childIndex, bool force = false, bool autoScroll = true) { if (childIndex >= Content.CountChildren || childIndex < 0) return; GUIComponent child = Content.GetChild(childIndex); bool wasSelected = true; if (OnSelected != null) wasSelected = force || OnSelected(child, child.UserData); if (!wasSelected) return; if (SelectMultiple) { if (selected.Contains(child)) { selected.Remove(child); } else { selected.Add(child); } } else { selected.Clear(); selected.Add(child); } // Ensure that the selected element is visible. This may not be the case, if the selection is run from code. (e.g. if we have two list boxes that are synced) // TODO: This method only works when moving one item up/down (e.g. when using the up and down arrows) if (autoScroll) { if (ScrollBar.IsHorizontal) { if (child.Rect.X < MouseRect.X) { //child outside the left edge of the frame -> move left ScrollBar.BarScroll -= (float)(MouseRect.X - child.Rect.X) / (totalSize - Content.Rect.Width); } else if (child.Rect.Right > MouseRect.Right) { //child outside the right edge of the frame -> move right ScrollBar.BarScroll += (float)(child.Rect.Right - MouseRect.Right) / (totalSize - Content.Rect.Width); } } else { if (child.Rect.Y < MouseRect.Y) { //child above the top of the frame -> move up ScrollBar.BarScroll -= (float)(MouseRect.Y - child.Rect.Y) / (totalSize - Content.Rect.Height); } else if (child.Rect.Bottom > MouseRect.Bottom) { //child below the bottom of the frame -> move down ScrollBar.BarScroll += (float)(child.Rect.Bottom - MouseRect.Bottom) / (totalSize - Content.Rect.Height); } } } // If one of the children is the subscriber, we don't want to register, because it will unregister the child. if (RectTransform.GetAllChildren().None(rt => rt.GUIComponent == GUI.KeyboardDispatcher.Subscriber)) { Selected = true; GUI.KeyboardDispatcher.Subscriber = this; } } public void Deselect() { Selected = false; if (GUI.KeyboardDispatcher.Subscriber == this) { GUI.KeyboardDispatcher.Subscriber = null; } selected.Clear(); } public void UpdateScrollBarSize() { if (Content == null) return; totalSize = 0; var children = Content.Children.Where(c => c.Visible); if (useGridLayout) { int pos = 0; foreach (GUIComponent child in children) { if (ScrollBar.IsHorizontal) { if (pos + child.Rect.Height + spacing > Content.Rect.Height || child == children.Last()) { pos = 0; totalSize += child.Rect.Width + spacing; } else { pos += child.Rect.Height + spacing; } } else { if (pos + child.Rect.Width + spacing > Content.Rect.Width || child == children.Last()) { pos = 0; totalSize += child.Rect.Height + spacing; } else { pos += child.Rect.Width + spacing; } } } } else { foreach (GUIComponent child in children) { totalSize += (ScrollBar.IsHorizontal) ? child.Rect.Width : child.Rect.Height; } totalSize += Content.CountChildren * spacing; } ScrollBar.BarSize = ScrollBar.IsHorizontal ? Math.Max(Math.Min(Content.Rect.Width / (float)totalSize, 1.0f), 5.0f / Content.Rect.Width) : Math.Max(Math.Min(Content.Rect.Height / (float)totalSize, 1.0f), 5.0f / Content.Rect.Height); } public override void ClearChildren() { Content.ClearChildren(); selected.Clear(); } public override void RemoveChild(GUIComponent child) { if (child == null) return; child.RectTransform.Parent = null; if (selected.Contains(child)) selected.Remove(child); UpdateScrollBarSize(); } public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) { //do nothing (the children have to be drawn in the Draw method after the ScissorRectangle has been set) return; } protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) return; Content.DrawManually(spriteBatch, alsoChildren: false); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; RasterizerState prevRasterizerState = spriteBatch.GraphicsDevice.RasterizerState; if (HideChildrenOutsideFrame) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); } var children = Content.Children; int lastVisible = 0; int i = 0; foreach (GUIComponent child in Content.Children) { if (!child.Visible) continue; if (!IsChildInsideFrame(child)) { if (lastVisible > 0) break; continue; } lastVisible = i; child.DrawManually(spriteBatch, alsoChildren: true, recursive: true); i++; } if (HideChildrenOutsideFrame) { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: prevRasterizerState); } if (ScrollBar.Visible) ScrollBar.DrawManually(spriteBatch, alsoChildren: true, recursive: true); } private bool IsChildInsideFrame(GUIComponent child) { if (child == null) return false; if (ScrollBar.IsHorizontal) { if (child.Rect.Right < Content.Rect.X) return false; if (child.Rect.X > Content.Rect.Right) return false; } else { if (child.Rect.Bottom < Content.Rect.Y) return false; if (child.Rect.Y > Content.Rect.Bottom) return false; } return true; } public void ReceiveTextInput(char inputChar) { GUI.KeyboardDispatcher.Subscriber = null; } public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } public void ReceiveSpecialInput(Keys key) { switch (key) { case Keys.Down: SelectNext(); break; case Keys.Up: SelectPrevious(); break; default: GUI.KeyboardDispatcher.Subscriber = null; break; } } } }