using System; using System.Collections.Generic; using System.Linq; using Robust.Client.Graphics; using Robust.Shared.Maths; namespace Robust.Client.UserInterface.Controls { /// /// Option button which allows toggling multiple elements. /// /// type to use as the unique key for each option. Functions similarly /// to dictionary key, so the type should make sure to respect dictionary key semantics. public class MultiselectOptionButton : ContainerButton where TKey : notnull { public const string StyleClassOptionButton = "optionButton"; public const string StyleClassOptionTriangle = "optionTriangle"; private List _buttonData = new(); // map from key to buttondata index private Dictionary _keyMap = new(); private readonly Popup _popup; private readonly VBoxContainer _popupVBox; private readonly Label _label; public event Action? OnItemSelected; /// /// Tracks the order in which items were selected, latest going at the end. /// private List _selectedKeys = new(); /// /// Ids of all currently selected items, ordered by most recently selected = last /// public IReadOnlyList SelectedKeys => _selectedKeys; public int ItemCount => _buttonData.Count; /// /// Labels of all currently selected items, ordered by most recently selected = last /// public IEnumerable SelectedLabels => _selectedKeys .Select(key => _buttonData[_keyMap[key]].Button.Label.Text); /// /// Metadata of all currently selected items, ordered by most recently selected = last /// public IEnumerable SelectedMetadata => _selectedKeys .Select(key => _buttonData[_keyMap[key]].Metadata); public string? Label { get => _label.Text; set => _label.Text = value; } public MultiselectOptionButton() { AddStyleClass(StyleClassButton); OnPressed += OnPressedInternal; var hBox = new HBoxContainer(); AddChild(hBox); _popup = new Popup(); _popupVBox = new VBoxContainer(); _popup.AddChild(_popupVBox); _popup.OnPopupHide += OnPopupHide; _label = new Label { StyleClasses = { StyleClassOptionButton }, HorizontalExpand = true, }; hBox.AddChild(_label); var textureRect = new TextureRect { StyleClasses = { StyleClassOptionTriangle }, VerticalAlignment = VAlignment.Center, }; hBox.AddChild(textureRect); } public void AddItem(Texture icon, string label, TKey key) { AddItem(label, key); } public void AddItem(string label, TKey key) { if (_keyMap.ContainsKey(key)) { throw new ArgumentException("An item with the same key already exists."); } var button = new Button { Text = label, ToggleMode = true }; button.OnPressed += ButtonOnPressed; var data = new ButtonData(label, button, key); _keyMap.Add(key, _buttonData.Count); _buttonData.Add(data); _popupVBox.AddChild(button); } private void TogglePopup(bool show) { if (show) { var globalPos = GlobalPosition; _popupVBox.Measure(Vector2.Infinity); var (minX, minY) = _popupVBox.DesiredSize; var box = UIBox2.FromDimensions(globalPos, (Math.Max(minX, Width), minY)); UserInterfaceManager.ModalRoot.AddChild(_popup); _popup.Open(box); } else { _popup.Close(); } } private void OnPopupHide() { UserInterfaceManager.ModalRoot.RemoveChild(_popup); } private void ButtonOnPressed(ButtonEventArgs obj) { TogglePopup(false); foreach (var buttonData in _buttonData) { if (buttonData.Button == obj.Button) { if (obj.Button.Pressed) { _selectedKeys.Add(buttonData.Key); } else { _selectedKeys.Remove(buttonData.Key); } OnItemSelected?.Invoke(new ItemPressedEventArgs(buttonData.Key, obj.Button.Pressed, this)); return; } } // Not reachable. throw new InvalidOperationException(); } public void Clear() { _keyMap.Clear(); foreach (var buttonDatum in _buttonData) { buttonDatum.Button.OnPressed -= ButtonOnPressed; } _buttonData.Clear(); _popupVBox.DisposeAllChildren(); _selectedKeys = new List(); } public TKey GetItemKey(int idx) { return _buttonData[idx].Key; } public object? GetItemMetadata(int idx) { return _buttonData[idx].Metadata; } public bool IsItemDisabled(int idx) { return _buttonData[idx].Disabled; } public void RemoveItem(int idx) { var data = _buttonData[idx]; data.Button.OnPressed -= ButtonOnPressed; _keyMap.Remove(data.Key); _popupVBox.RemoveChild(data.Button); _buttonData.RemoveAt(idx); var newIdx = 0; foreach (var buttonData in _buttonData) { _keyMap[buttonData.Key] = newIdx++; } } public void Select(int idx) { var data = _buttonData[idx]; if (data.Button.Pressed) return; _selectedKeys.Add(data.Key); data.Button.Pressed = true; } public void SelectKey(TKey key) { Select(GetIdx(key)); } public void DeselectAll() { foreach (var buttonData in _buttonData) { Deselect(buttonData); } } public void Deselect(int idx) { Deselect(_buttonData[idx]); } public void DeselectKey(TKey key) { Deselect(GetIdx(key)); } private void Deselect(ButtonData data) { if (!data.Button.Pressed) return; _selectedKeys.Remove(data.Key); data.Button.Pressed = false; } public int GetIdx(TKey key) { return _keyMap[key]; } public void SetItemDisabled(int idx, bool disabled) { var data = _buttonData[idx]; data.Disabled = disabled; data.Button.Disabled = disabled; } public void SetItemKey(int idx, TKey key) { if (_keyMap.TryGetValue(key, out var existIdx) && existIdx != idx) { throw new InvalidOperationException("An item with said key already exists."); } var data = _buttonData[idx]; _keyMap.Remove(data.Key); _keyMap.Add(key, idx); data.Key = key; } public void SetItemMetadata(int idx, object metadata) { _buttonData[idx].Metadata = metadata; } public void SetItemText(int idx, string text) { var data = _buttonData[idx]; data.Text = text; data.Button.Text = text; } private void OnPressedInternal(ButtonEventArgs args) { TogglePopup(true); } protected override void ExitedTree() { base.ExitedTree(); TogglePopup(false); } public class ItemPressedEventArgs : EventArgs { public readonly MultiselectOptionButton Button; /// /// True if item is being selected, false if being unselected /// public readonly bool Selected; /// /// True if item is being deselected, false if being selected /// public bool Deselected => !Selected; /// /// The key of the item that has been selected or deselected. /// public readonly TKey Key; public ItemPressedEventArgs(TKey key, bool selected, MultiselectOptionButton button) { Key = key; Selected = selected; Button = button; } } private sealed class ButtonData { public string Text; public bool Disabled; public object? Metadata; public TKey Key; public Button Button; public ButtonData(string text, Button button, TKey key) { Text = text; Button = button; Key = key; } } } }