Files
RobustToolbox/Robust.Client/UserInterface/Controls/MultiselectOptionButton.cs
Pieter-Jan Briers 583b7ebf38 WPF layout (#1581)
2021-02-21 12:28:13 +01:00

327 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// Option button which allows toggling multiple elements.
/// </summary>
/// <typeparam name="TKey">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.</typeparam>
public class MultiselectOptionButton<TKey> : ContainerButton where TKey : notnull
{
public const string StyleClassOptionButton = "optionButton";
public const string StyleClassOptionTriangle = "optionTriangle";
private List<ButtonData> _buttonData = new();
// map from key to buttondata index
private Dictionary<TKey, int> _keyMap = new();
private readonly Popup _popup;
private readonly VBoxContainer _popupVBox;
private readonly Label _label;
public event Action<ItemPressedEventArgs>? OnItemSelected;
/// <summary>
/// Tracks the order in which items were selected, latest going at the end.
/// </summary>
private List<TKey> _selectedKeys = new();
/// <summary>
/// Ids of all currently selected items, ordered by most recently selected = last
/// </summary>
public IReadOnlyList<TKey> SelectedKeys => _selectedKeys;
public int ItemCount => _buttonData.Count;
/// <summary>
/// Labels of all currently selected items, ordered by most recently selected = last
/// </summary>
public IEnumerable<string?> SelectedLabels => _selectedKeys
.Select(key => _buttonData[_keyMap[key]].Button.Label.Text);
/// <summary>
/// Metadata of all currently selected items, ordered by most recently selected = last
/// </summary>
public IEnumerable<object?> 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<TKey>();
}
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<TKey> Button;
/// <summary>
/// True if item is being selected, false if being unselected
/// </summary>
public readonly bool Selected;
/// <summary>
/// True if item is being deselected, false if being selected
/// </summary>
public bool Deselected => !Selected;
/// <summary>
/// The key of the item that has been selected or deselected.
/// </summary>
public readonly TKey Key;
public ItemPressedEventArgs(TKey key, bool selected, MultiselectOptionButton<TKey> 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;
}
}
}
}