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

View File

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

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Client.Administration.Systems; using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
@@ -28,15 +29,14 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
private bool _ascending = true; private bool _ascending = true;
private bool _showDisconnected; private bool _showDisconnected;
public event Action<PlayerTabEntry, GUIBoundKeyEventArgs>? OnEntryKeyBindDown; public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab() public PlayerTab()
{ {
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_adminSystem = _entManager.System<AdminSystem>();
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
RefreshPlayerList(_adminSystem.PlayerList);
_adminSystem = _entManager.System<AdminSystem>();
_adminSystem.PlayerListChanged += RefreshPlayerList; _adminSystem.PlayerListChanged += RefreshPlayerList;
_adminSystem.OverlayEnabled += OverlayEnabled; _adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled; _adminSystem.OverlayDisabled += OverlayDisabled;
@@ -46,8 +46,17 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor); ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
ListHeader.OnHeaderClicked += HeaderClicked; 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() private void OverlayEnabled()
{ {
OverlayButton.Pressed = true; OverlayButton.Pressed = true;
@@ -70,6 +79,8 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
} }
} }
#endregion
private void ShowDisconnectedPressed(ButtonEventArgs args) private void ShowDisconnectedPressed(ButtonEventArgs args)
{ {
_showDisconnected = args.Button.Pressed; _showDisconnected = args.Button.Pressed;
@@ -92,14 +103,10 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
} }
} }
#region ListContainer
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players) private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{ {
foreach (var child in PlayerList.Children.ToArray())
{
if (child is PlayerTabEntry)
child.Dispose();
}
_players = players; _players = players;
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}"; PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
@@ -108,29 +115,66 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
UpdateHeaderSymbols(); UpdateHeaderSymbols();
var useAltColor = false; SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
foreach (var player in sortedPlayers) $"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
{ .ToList());
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;
}
} }
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() private void UpdateHeaderSymbols()
{ {
ListHeader.ResetHeaderText(); ListHeader.ResetHeaderText();
@@ -174,5 +218,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
RefreshPlayerList(_adminSystem.PlayerList); RefreshPlayerList(_adminSystem.PlayerList);
} }
#endregion
} }
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
} }

View File

@@ -1,6 +1,6 @@
<ContainerButton xmlns="https://spacestation14.io" <PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"> xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
<PanelContainer Name="BackgroundColorPanel"/> Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal" <BoxContainer Orientation="Horizontal"
HorizontalExpand="True" HorizontalExpand="True"
SeparationOverride="4"> SeparationOverride="4">
@@ -29,4 +29,4 @@
HorizontalExpand="True" HorizontalExpand="True"
ClipText="True"/> ClipText="True"/>
</BoxContainer> </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.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@@ -6,23 +7,24 @@ using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.PlayerTab; namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class PlayerTabEntry : ContainerButton public sealed partial class PlayerTabEntry : PanelContainer
{ {
public NetEntity? PlayerEntity; 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); RobustXamlLoader.Load(this);
UsernameLabel.Text = username; UsernameLabel.Text = player.Username;
if (!connected) if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled"); UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = job; JobLabel.Text = player.StartingJob;
CharacterLabel.Text = character; CharacterLabel.Text = player.CharacterName;
if (identity != character) if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{identity}]"; CharacterLabel.Text += $" [{player.IdentityName}]";
AntagonistLabel.Text = antagonist; AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
BackgroundColorPanel.PanelOverride = styleBox; BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = overallPlaytime; OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;
} }
} }

View File

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

View File

@@ -8,7 +8,8 @@ using Robust.Shared.Map;
namespace Content.Client.UserInterface.Controls; namespace Content.Client.UserInterface.Controls;
public sealed class ListContainer : Control [Virtual]
public class ListContainer : Control
{ {
public const string StylePropertySeparation = "separation"; public const string StylePropertySeparation = "separation";
public const string StyleClassListContainerButton = "list-container-button"; public const string StyleClassListContainerButton = "list-container-button";
@@ -21,9 +22,26 @@ public sealed class ListContainer : Control
set => _buttonGroup = value ? new ButtonGroup() : null; set => _buttonGroup = value ? new ButtonGroup() : null;
} }
public bool Toggle { get; set; } 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<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; public IReadOnlyList<ListData> Data => _data;
private const int DefaultSeparation = 3; private const int DefaultSeparation = 3;
@@ -72,11 +90,11 @@ public sealed class ListContainer : Control
_vScrollBar.OnValueChanged += ScrollValueChanged; _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) 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); GenerateItem?.Invoke(data[0], control);
control.Measure(Vector2Helpers.Infinity); control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y; _itemHeight = control.DesiredSize.Y;
@@ -97,7 +115,7 @@ public sealed class ListContainer : Control
if (_selected != null && !data.Contains(_selected)) if (_selected != null && !data.Contains(_selected))
{ {
_selected = null; _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) if (_buttons.TryGetValue(data, out var button) && Toggle)
button.Pressed = true; button.Pressed = true;
_selected = data; _selected = data;
button ??= new ListContainerButton(data); button ??= new ListContainerButton(data, _data.IndexOf(data));
OnItemPressed(new BaseButton.ButtonEventArgs(button, OnItemPressed(new BaseButton.ButtonEventArgs(button,
new GUIBoundKeyEventArgs(EngineKeyFunctions.UIClick, BoundKeyState.Up, new GUIBoundKeyEventArgs(EngineKeyFunctions.UIClick, BoundKeyState.Up,
new ScreenCoordinates(0, 0, WindowId.Main), true, Vector2.Zero, Vector2.Zero))); new ScreenCoordinates(0, 0, WindowId.Main), true, Vector2.Zero, Vector2.Zero)));
@@ -260,7 +278,7 @@ public sealed class ListContainer : Control
toRemove.Remove(data); toRemove.Remove(data);
else else
{ {
button = new ListContainerButton(data); button = new ListContainerButton(data, i);
button.OnPressed += OnItemPressed; button.OnPressed += OnItemPressed;
button.OnKeyBindDown += args => OnItemKeyBindDown(button, args); button.OnKeyBindDown += args => OnItemKeyBindDown(button, args);
button.ToggleMode = Toggle; button.ToggleMode = Toggle;
@@ -360,11 +378,14 @@ public sealed class ListContainer : Control
public sealed class ListContainerButton : ContainerButton, IEntityControl public sealed class ListContainerButton : ContainerButton, IEntityControl
{ {
public readonly ListData Data; public readonly ListData Data;
public readonly int Index;
// public PanelContainer Background; // public PanelContainer Background;
public ListContainerButton(ListData data) public ListContainerButton(ListData data, int index)
{ {
Data = data; Data = data;
Index = index;
// AddChild(Background = new PanelContainer // AddChild(Background = new PanelContainer
// { // {
// HorizontalExpand = true, // 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; return;
var entity = entry.PlayerEntity.Value; if (info.NetEntity == null)
return;
var entity = info.NetEntity.Value;
var function = args.Function; var function = args.Function;
if (function == EngineKeyFunctions.UIClick) if (function == EngineKeyFunctions.UIClick)

View File

@@ -12,3 +12,5 @@ bwoink-system-typing-indicator = {$players} {$count ->
} typing... } typing...
admin-bwoink-play-sound = Bwoink? 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-show-disconnected = Show Disconnected
player-tab-overlay = Overlay player-tab-overlay = Overlay
player-tab-entry-tooltip = Playtime is displayed in days:hours:minutes. 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