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;
+ }
+
+ Title = $"{sel.CharacterName} / {sel.Username}";
- if (sel.OverallPlaytime != null)
- {
- Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
- }
+ if (sel.OverallPlaytime != null)
+ {
+ Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
};
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;
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})
<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>
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;
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;
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;
}
}
+ #endregion
+
private void ShowDisconnectedPressed(ButtonEventArgs args)
{
_showDisconnected = args.Button.Pressed;
}
}
+ #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}";
UpdateHeaderSymbols();
- var useAltColor = false;
- foreach (var player in sortedPlayers)
+ 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 (!_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;
+ 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();
RefreshPlayerList(_adminSystem.PlayerList);
}
+
+ #endregion
}
+
+ public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
}
-<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">
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>
-</ContainerButton>
+</PanelContainer>
-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;
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;
}
}
HorizontalExpand="True"
ClipText="True"
Text="{Loc player-tab-playtime}"
- MouseFilter="Pass"/>
+ MouseFilter="Pass"
+ ToolTip="{Loc player-tab-entry-tooltip}"/>
</BoxContainer>
</Control>
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";
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;
_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;
if (_selected != null && !data.Contains(_selected))
{
_selected = null;
- ItemPressed?.Invoke(null, null);
+ NoItemSelected?.Invoke();
}
}
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)));
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;
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,
--- /dev/null
+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);
+ }
+}
}
}
- 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)
} typing...
admin-bwoink-play-sound = Bwoink?
+
+bwoink-title-none-selected = None selected
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