Add search filter to the admin menu player tab (#28030)

This commit is contained in:
ShadowCommander
2024-05-30 23:28:08 -07:00
committed by GitHub
parent dbcdefc5fd
commit 3ed1ee6a5b
12 changed files with 236 additions and 80 deletions

View File

@@ -16,14 +16,17 @@ namespace Content.Client.Administration.UI.Bwoink
Bwoink.ChannelSelector.OnSelectionChanged += sel =>
{
if (sel is not null)
if (sel is null)
{
Title = $"{sel.CharacterName} / {sel.Username}";
Title = Loc.GetString("bwoink-none-selected");
return;
}
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
Title = $"{sel.CharacterName} / {sel.Username}";
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
};

View File

@@ -20,7 +20,7 @@ namespace Content.Client.Administration.UI.CustomControls
private List<PlayerInfo> _playerList = new();
private readonly List<PlayerInfo> _sortedPlayerList = new();
public event Action<PlayerInfo>? OnSelectionChanged;
public event Action<PlayerInfo?>? OnSelectionChanged;
public IReadOnlyList<PlayerInfo> PlayerInfo => _playerList;
public Func<PlayerInfo, string, string>? OverrideText;
@@ -41,12 +41,19 @@ namespace Content.Client.Administration.UI.CustomControls
PlayerListContainer.ItemPressed += PlayerListItemPressed;
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
PlayerListContainer.GenerateItem += GenerateButton;
PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
PopulateList(_adminSystem.PlayerList);
FilterLineEdit.OnTextChanged += _ => FilterList();
_adminSystem.PlayerListChanged += PopulateList;
BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)};
}
private void PlayerListNoItemSelected()
{
_selectedPlayer = null;
OnSelectionChanged?.Invoke(null);
}
private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData {Info: var selectedPlayer})

View File

@@ -1,21 +1,19 @@
<Control xmlns="https://spacestation14.io"
xmlns:pt="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="PlayerCount" HorizontalExpand="True" SizeFlagsStretchRatio="0.50"
Text="{Loc Player Count}" />
<Button Name="ShowDisconnectedButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
Text="{Loc player-tab-show-disconnected}" ToggleMode="True"/>
<Button Name="OverlayButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"
Text="{Loc player-tab-overlay}" ToggleMode="True"/>
<Label Name="PlayerCount" HorizontalExpand="True" Text="{Loc Player Count}" />
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
PlaceHolder="{Loc player-tab-filter-line-edit-placeholder}" />
<Button Name="ShowDisconnectedButton" HorizontalExpand="True"
Text="{Loc player-tab-show-disconnected}" ToggleMode="True" />
<Button Name="OverlayButton" HorizontalExpand="True" Text="{Loc player-tab-overlay}" ToggleMode="True" />
</BoxContainer>
<Control MinSize="0 5" />
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="PlayerList">
<pt:PlayerTabHeader Name="ListHeader" />
<cc:HSeparator />
</BoxContainer>
</ScrollContainer>
<Control MinSize="0 5"/>
<pt:PlayerTabHeader Name="ListHeader"/>
<cc:HSeparator/>
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True"/>
</BoxContainer>
</Control>

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -28,15 +29,14 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
private bool _ascending = true;
private bool _showDisconnected;
public event Action<PlayerTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab()
{
IoCManager.InjectDependencies(this);
_adminSystem = _entManager.System<AdminSystem>();
RobustXamlLoader.Load(this);
RefreshPlayerList(_adminSystem.PlayerList);
_adminSystem = _entManager.System<AdminSystem>();
_adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
@@ -46,8 +46,17 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
ListHeader.OnHeaderClicked += HeaderClicked;
SearchList.SearchBar = SearchLineEdit;
SearchList.GenerateItem += GenerateButton;
SearchList.DataFilterCondition += DataFilterCondition;
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
RefreshPlayerList(_adminSystem.PlayerList);
}
#region Antag Overlay
private void OverlayEnabled()
{
OverlayButton.Pressed = true;
@@ -70,6 +79,8 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
}
}
#endregion
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
@@ -92,14 +103,10 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
}
}
#region ListContainer
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
foreach (var child in PlayerList.Children.ToArray())
{
if (child is PlayerTabEntry)
child.Dispose();
}
_players = players;
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
@@ -108,29 +115,66 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
UpdateHeaderSymbols();
var useAltColor = false;
foreach (var player in sortedPlayers)
{
if (!_showDisconnected && !player.Connected)
continue;
var entry = new PlayerTabEntry(player.Username,
player.CharacterName,
player.IdentityName,
player.StartingJob,
player.Antag ? "YES" : "NO",
new StyleBoxFlat(useAltColor ? _altColor : _defaultColor),
player.Connected,
player.PlaytimeString);
entry.PlayerEntity = player.NetEntity;
entry.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(entry, args);
entry.ToolTip = Loc.GetString("player-tab-entry-tooltip");
PlayerList.AddChild(entry);
useAltColor ^= true;
}
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
.ToList());
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not PlayerListData { Info: var player})
return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
}
/// <summary>
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
/// If all characters are lowercase, the comparison ignores case.
/// If there is an uppercase character, the comparison is case sensitive.
/// </summary>
/// <param name="filter"></param>
/// <param name="listData"></param>
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
private bool DataFilterCondition(string filter, ListData listData)
{
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
return false;
if (!_showDisconnected && !info.Connected)
return false;
if (IsAllLower(filter))
{
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
return false;
}
else
{
if (!playerString.Contains(filter))
return false;
}
return true;
}
private bool IsAllLower(string input)
{
foreach (var c in input)
{
if (char.IsLetter(c) && !char.IsLower(c))
return false;
}
return true;
}
#endregion
#region Header
private void UpdateHeaderSymbols()
{
ListHeader.ResetHeaderText();
@@ -174,5 +218,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
RefreshPlayerList(_adminSystem.PlayerList);
}
#endregion
}
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
}

View File

@@ -1,6 +1,6 @@
<ContainerButton xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer Name="BackgroundColorPanel"/>
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
@@ -29,4 +29,4 @@
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>
</ContainerButton>
</PanelContainer>

View File

@@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -6,23 +7,24 @@ using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences]
public sealed partial class PlayerTabEntry : ContainerButton
public sealed partial class PlayerTabEntry : PanelContainer
{
public NetEntity? PlayerEntity;
public PlayerTabEntry(string username, string character, string identity, string job, string antagonist, StyleBox styleBox, bool connected, string overallPlaytime)
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
{
RobustXamlLoader.Load(this);
UsernameLabel.Text = username;
if (!connected)
UsernameLabel.Text = player.Username;
if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = job;
CharacterLabel.Text = character;
if (identity != character)
CharacterLabel.Text += $" [{identity}]";
AntagonistLabel.Text = antagonist;
BackgroundColorPanel.PanelOverride = styleBox;
OverallPlaytimeLabel.Text = overallPlaytime;
JobLabel.Text = player.StartingJob;
CharacterLabel.Text = player.CharacterName;
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;
}
}

View File

@@ -37,6 +37,7 @@
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-playtime}"
MouseFilter="Pass"/>
MouseFilter="Pass"
ToolTip="{Loc player-tab-entry-tooltip}"/>
</BoxContainer>
</Control>

View File

@@ -8,7 +8,8 @@ using Robust.Shared.Map;
namespace Content.Client.UserInterface.Controls;
public sealed class ListContainer : Control
[Virtual]
public class ListContainer : Control
{
public const string StylePropertySeparation = "separation";
public const string StyleClassListContainerButton = "list-container-button";
@@ -21,9 +22,26 @@ public sealed class ListContainer : Control
set => _buttonGroup = value ? new ButtonGroup() : null;
}
public bool Toggle { get; set; }
/// <summary>
/// Called when creating a button on the UI.
/// The provided <see cref="ListContainerButton"/> is the generated button that Controls should be parented to.
/// </summary>
public Action<ListData, ListContainerButton>? GenerateItem;
public Action<BaseButton.ButtonEventArgs?, ListData?>? ItemPressed;
public Action<GUIBoundKeyEventArgs, ListData?>? ItemKeyBindDown;
/// <inheritdoc cref="BaseButton.OnPressed"/>
public Action<BaseButton.ButtonEventArgs, ListData>? ItemPressed;
/// <summary>
/// Invoked when a KeyBind is pressed on a ListContainerButton.
/// </summary>
public Action<GUIBoundKeyEventArgs, ListData>? ItemKeyBindDown;
/// <summary>
/// Invoked when the selected item does not exist in the new data when PopulateList is called.
/// </summary>
public Action? NoItemSelected;
public IReadOnlyList<ListData> Data => _data;
private const int DefaultSeparation = 3;
@@ -72,11 +90,11 @@ public sealed class ListContainer : Control
_vScrollBar.OnValueChanged += ScrollValueChanged;
}
public void PopulateList(IReadOnlyList<ListData> data)
public virtual void PopulateList(IReadOnlyList<ListData> data)
{
if ((_itemHeight == 0 || _data is {Count: 0}) && data.Count > 0)
{
ListContainerButton control = new(data[0]);
ListContainerButton control = new(data[0], 0);
GenerateItem?.Invoke(data[0], control);
control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y;
@@ -97,7 +115,7 @@ public sealed class ListContainer : Control
if (_selected != null && !data.Contains(_selected))
{
_selected = null;
ItemPressed?.Invoke(null, null);
NoItemSelected?.Invoke();
}
}
@@ -116,7 +134,7 @@ public sealed class ListContainer : Control
if (_buttons.TryGetValue(data, out var button) && Toggle)
button.Pressed = true;
_selected = data;
button ??= new ListContainerButton(data);
button ??= new ListContainerButton(data, _data.IndexOf(data));
OnItemPressed(new BaseButton.ButtonEventArgs(button,
new GUIBoundKeyEventArgs(EngineKeyFunctions.UIClick, BoundKeyState.Up,
new ScreenCoordinates(0, 0, WindowId.Main), true, Vector2.Zero, Vector2.Zero)));
@@ -260,7 +278,7 @@ public sealed class ListContainer : Control
toRemove.Remove(data);
else
{
button = new ListContainerButton(data);
button = new ListContainerButton(data, i);
button.OnPressed += OnItemPressed;
button.OnKeyBindDown += args => OnItemKeyBindDown(button, args);
button.ToggleMode = Toggle;
@@ -360,11 +378,14 @@ public sealed class ListContainer : Control
public sealed class ListContainerButton : ContainerButton, IEntityControl
{
public readonly ListData Data;
public readonly int Index;
// public PanelContainer Background;
public ListContainerButton(ListData data)
public ListContainerButton(ListData data, int index)
{
Data = data;
Index = index;
// AddChild(Background = new PanelContainer
// {
// HorizontalExpand = true,

View File

@@ -0,0 +1,68 @@
using System.Linq;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
public sealed class SearchListContainer : ListContainer
{
private LineEdit? _searchBar;
private List<ListData> _unfilteredData = new();
/// <summary>
/// The <see cref="LineEdit"/> that is used to filter the list data.
/// </summary>
public LineEdit? SearchBar
{
get => _searchBar;
set
{
if (_searchBar is not null)
_searchBar.OnTextChanged -= FilterList;
_searchBar = value;
if (_searchBar is null)
return;
_searchBar.OnTextChanged += FilterList;
}
}
/// <summary>
/// Runs over the ListData to determine if it should pass the filter.
/// </summary>
public Func<string, ListData, bool>? DataFilterCondition = null;
public override void PopulateList(IReadOnlyList<ListData> data)
{
_unfilteredData = data.ToList();
FilterList();
}
private void FilterList(LineEdit.LineEditEventArgs obj)
{
FilterList();
}
private void FilterList()
{
var filterText = SearchBar?.Text;
if (DataFilterCondition is null || string.IsNullOrEmpty(filterText))
{
base.PopulateList(_unfilteredData);
return;
}
var filteredData = new List<ListData>();
foreach (var data in _unfilteredData)
{
if (!DataFilterCondition(filterText, data))
continue;
filteredData.Add(data);
}
base.PopulateList(filteredData);
}
}

View File

@@ -177,12 +177,15 @@ public sealed class AdminUIController : UIController,
}
}
private void PlayerTabEntryKeyBindDown(PlayerTabEntry entry, GUIBoundKeyEventArgs args)
private void PlayerTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data)
{
if (entry.PlayerEntity == null)
if (data is not PlayerListData {Info: var info})
return;
var entity = entry.PlayerEntity.Value;
if (info.NetEntity == null)
return;
var entity = info.NetEntity.Value;
var function = args.Function;
if (function == EngineKeyFunctions.UIClick)

View File

@@ -12,3 +12,5 @@ bwoink-system-typing-indicator = {$players} {$count ->
} typing...
admin-bwoink-play-sound = Bwoink?
bwoink-title-none-selected = None selected

View File

@@ -6,3 +6,6 @@ player-tab-playtime = Playtime
player-tab-show-disconnected = Show Disconnected
player-tab-overlay = Overlay
player-tab-entry-tooltip = Playtime is displayed in days:hours:minutes.
player-tab-filter-line-edit-placeholder = Filter
player-tab-is-antag-yes = YES
player-tab-is-antag-no = NO