Files
tbd-station-14/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
Tayrtahn f09bade8e7 Validate remaining ProtoId strings (#38747)
Validate remaining ProtoId strings
2025-07-04 22:55:45 +02:00

452 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Linq;
using System.Numerics;
namespace Content.Client.Access.UI;
[GenerateTypedNameReferences]
public sealed partial class GroupedAccessLevelChecklist : BoxContainer
{
private static readonly ProtoId<AccessGroupPrototype> GeneralAccessGroup = "General";
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private bool _isMonotone;
private string? _labelStyleClass;
// Access data
private HashSet<ProtoId<AccessGroupPrototype>> _accessGroups = new();
private HashSet<ProtoId<AccessLevelPrototype>> _accessLevels = new();
private HashSet<ProtoId<AccessLevelPrototype>> _activeAccessLevels = new();
// Button groups
private readonly ButtonGroup _accessGroupsButtons = new();
// Temp values
private int _accessGroupTabIndex = 0;
private bool _canInteract = false;
private List<AccessLevelPrototype> _accessLevelsForTab = new();
private readonly List<AccessLevelEntry> _accessLevelEntries = new();
private readonly Dictionary<AccessGroupPrototype, List<AccessLevelPrototype>> _groupedAccessLevels = new();
// Events
public event Action<HashSet<ProtoId<AccessLevelPrototype>>, bool>? OnAccessLevelsChangedEvent;
/// <summary>
/// Creates a UI control for changing access levels.
/// Access levels are organized under a list of tabs by their associated access group.
/// </summary>
public GroupedAccessLevelChecklist()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
private void ArrangeAccessControls()
{
// Create a list of known access groups with which to populate the UI
_groupedAccessLevels.Clear();
foreach (var accessGroup in _accessGroups)
{
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
continue;
_groupedAccessLevels.Add(accessGroupProto, new());
}
// Ensure that the 'general' access group is added to handle
// misc. access levels that aren't associated with any group
if (_protoManager.TryIndex(GeneralAccessGroup, out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in _accessLevels)
{
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
continue;
var assigned = false;
foreach (var (accessGroup, accessLevels) in _groupedAccessLevels)
{
if (!accessGroup.Tags.Contains(accessLevelProto.ID))
continue;
assigned = true;
_groupedAccessLevels[accessGroup].Add(accessLevelProto);
}
if (!assigned && generalAccessProto != null)
_groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
}
// Remove access groups that have no assigned access levels
foreach (var (group, accessLevels) in _groupedAccessLevels)
{
if (accessLevels.Count == 0)
_groupedAccessLevels.Remove(group);
}
}
private bool TryRebuildAccessGroupControls()
{
AccessGroupList.DisposeAllChildren();
AccessLevelChecklist.DisposeAllChildren();
// No access level prototypes were assigned to any of the access level groups.
// Either the turret controller has no assigned access levels or their names were invalid.
if (_groupedAccessLevels.Count == 0)
return false;
// Reorder the access groups alphabetically
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
// Add group access buttons to the UI
foreach (var accessGroup in orderedAccessGroups)
{
var accessGroupButton = CreateAccessGroupButton();
// Button styling
if (_groupedAccessLevels.Count > 1)
{
if (AccessGroupList.ChildCount == 0)
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
else
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
}
accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
// Label text and styling
if (_labelStyleClass != null)
accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass);
var accessLevelPrototypes = _groupedAccessLevels[accessGroup];
var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x))
? "»"
: accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x))
? ""
: " ";
var text = Loc.GetString(
"turret-controls-window-access-group-label",
("prefix", prefix),
("label", accessGroup.GetAccessGroupName())
);
accessGroupButton.Text = text;
// Button events
accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent());
AccessGroupList.AddChild(accessGroupButton);
}
// Adjust the current tab index so it remains in range
if (_accessGroupTabIndex >= _groupedAccessLevels.Count)
_accessGroupTabIndex = _groupedAccessLevels.Count - 1;
return true;
}
/// <summary>
/// Rebuilds the checkbox list for the access level controls.
/// </summary>
public void RebuildAccessLevelsControls()
{
AccessLevelChecklist.DisposeAllChildren();
_accessLevelEntries.Clear();
// No access level prototypes were assigned to any of the access level groups
// Either turret controller has no assigned access levels, or their names were invalid
if (_groupedAccessLevels.Count == 0)
return;
// Reorder the access groups alphabetically
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
// Get the access levels associated with the current tab
var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex];
_accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto];
_accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList();
// Add an 'all' checkbox as the first child of the list if it has more than one access level
// Toggling this checkbox on will mark all other boxes below it on/off
var allCheckBox = CreateAccessLevelCheckbox();
allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox");
if (_labelStyleClass != null)
allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
// Add the 'all' checkbox events
allCheckBox.OnPressed += args =>
{
SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed);
var accessLevels = new HashSet<ProtoId<AccessLevelPrototype>>();
foreach (var accessLevel in _accessLevelsForTab)
{
accessLevels.Add(accessLevel);
}
OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed);
};
AccessLevelChecklist.AddChild(allCheckBox);
// Hide the 'all' checkbox if the tab has only one access level
var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
allCheckBox.Visible = allCheckBoxVisible;
allCheckBox.Disabled = !_canInteract;
// Add any remaining missing access level buttons to the UI
foreach (var accessLevel in _accessLevelsForTab)
{
// Create the entry
var accessLevelEntry = new AccessLevelEntry(_isMonotone);
accessLevelEntry.AccessLevel = accessLevel;
accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel);
accessLevelEntry.CheckBox.Disabled = !_canInteract;
if (_labelStyleClass != null)
accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
// Set the checkbox linkage lines
var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1);
var lines = new List<(Vector2, Vector2)>
{
(new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
(new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
};
accessLevelEntry.UpdateCheckBoxLink(lines);
accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White;
// Add checkbox events
accessLevelEntry.CheckBox.OnPressed += args =>
{
// If the checkbox and its siblings are checked, check the 'all' checkbox too
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed);
};
AccessLevelChecklist.AddChild(accessLevelEntry);
_accessLevelEntries.Add(accessLevelEntry);
}
// Press the 'all' checkbox if all others are pressed
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
}
private bool AreAllCheckBoxesPressed(IEnumerable<CheckBox> checkBoxes)
{
foreach (var checkBox in checkBoxes)
{
if (!checkBox.Pressed)
return false;
}
return true;
}
private void SetCheckBoxPressedState(List<AccessLevelEntry> accessLevelEntries, bool pressed)
{
foreach (var accessLevelEntry in accessLevelEntries)
{
accessLevelEntry.CheckBox.Pressed = pressed;
}
}
/// <summary>
/// Provides the UI with a list of access groups using which list of tabs should be populated.
/// </summary>
public void SetAccessGroups(HashSet<ProtoId<AccessGroupPrototype>> accessGroups)
{
_accessGroups = accessGroups;
ArrangeAccessControls();
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Provides the UI with a list of access levels with which it can populate the currently selected tab.
/// </summary>
public void SetAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> accessLevels)
{
_accessLevels = accessLevels;
ArrangeAccessControls();
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets which access level checkboxes should be marked on the UI.
/// </summary>
public void SetActiveAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> activeAccessLevels)
{
_activeAccessLevels = activeAccessLevels;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets whether the local player can interact with the checkboxes.
/// </summary>
public void SetLocalPlayerAccessibility(bool canInteract)
{
_canInteract = canInteract;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets whether the UI should use monotone buttons and checkboxes.
/// </summary>
public void SetMonotone(bool monotone)
{
_isMonotone = monotone;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Applies the specified style to the labels on the UI buttons and checkboxes.
/// </summary>
public void SetLabelStyleClass(string? styleClass)
{
_labelStyleClass = styleClass;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
private void OnAccessGroupChanged(int newTabIndex)
{
if (newTabIndex == _accessGroupTabIndex)
return;
_accessGroupTabIndex = newTabIndex;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
private Button CreateAccessGroupButton()
{
var button = _isMonotone ? new MonotoneButton() : new Button();
button.ToggleMode = true;
button.Group = _accessGroupsButtons;
button.Label.HorizontalAlignment = HAlignment.Left;
return button;
}
private CheckBox CreateAccessLevelCheckbox()
{
var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox();
checkbox.Margin = new Thickness(0, 0, 0, 3);
checkbox.ToggleMode = true;
checkbox.ReservesSpace = false;
return checkbox;
}
private sealed class AccessLevelEntry : BoxContainer
{
public ProtoId<AccessLevelPrototype> AccessLevel;
public readonly CheckBox CheckBox;
public readonly LineRenderer CheckBoxLink;
public AccessLevelEntry(bool monotone)
{
HorizontalExpand = true;
CheckBoxLink = new LineRenderer
{
SetWidth = 22,
VerticalExpand = true,
Margin = new Thickness(0, -1),
ReservesSpace = false,
};
AddChild(CheckBoxLink);
CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox();
CheckBox.ToggleMode = true;
CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f);
AddChild(CheckBox);
}
public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
{
CheckBoxLink.Lines = lines;
}
}
private sealed class LineRenderer : Control
{
/// <summary>
/// List of lines to render (their start and end x-y coordinates).
/// Position (0,0) is the top left corner of the control and
/// position (1,1) is the bottom right corner.
/// </summary>
/// <remarks>
/// The color of the lines is inherited from the control.
/// </remarks>
public List<(Vector2, Vector2)> Lines;
public LineRenderer()
{
Lines = new List<(Vector2, Vector2)>();
}
public LineRenderer(List<(Vector2, Vector2)> lines)
{
Lines = lines;
}
protected override void Draw(DrawingHandleScreen handle)
{
foreach (var line in Lines)
{
var start = PixelPosition +
new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y);
var end = PixelPosition +
new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y);
handle.DrawLine(start, end, ActualModulateSelf);
}
}
}
}