]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add search filter to the admin menu player tab (#28030)
authorShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Fri, 31 May 2024 06:28:08 +0000 (23:28 -0700)
committerGitHub <noreply@github.com>
Fri, 31 May 2024 06:28:08 +0000 (02:28 -0400)
12 files changed:
Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs
Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml
Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs
Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml
Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabEntry.xaml.cs
Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTabHeader.xaml
Content.Client/UserInterface/Controls/ListContainer.cs
Content.Client/UserInterface/Controls/SearchListContainer.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
Resources/Locale/en-US/administration/bwoink.ftl
Resources/Locale/en-US/administration/ui/tabs/player-tab.ftl

index f8d06f758f40dc227e277a7624cf50bbc95655af..999eba4d29dc5fffade91b711fe392a877afa4ff 100644 (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;
+                }
+
+                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}";
                 }
             };
 
index fdf935d7c0488985535e693eb8c469b70cdedf74..12522d552d71a898aeb5c469bcf0bd6972a76ff3 100644 (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})
index 3071bf8358b335dd1b8d2d91e24cc449cceb0742..25a96df1d377d30e2b078c63067491a426a24c61 100644 (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>
index 33a1d2361f28393a8e291e83f1ae562098aeaebf..a8bfaddecf45e5638d1791cdde924efee8af3c95 100644 (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)
+            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();
@@ -174,5 +218,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
 
             RefreshPlayerList(_adminSystem.PlayerList);
         }
+
+        #endregion
     }
+
+    public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
 }
index f9ed57792ed20409f86d7b593516eee745b4c0ef..e1371ec6f73b7b67670c4416527f12efee316e0e 100644 (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>
index 80a68f4cd2d5ce332d45d409810142341f42b4f8..89c5808afc7a7f5436ad8896de50bfd3038f8de4 100644 (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;
     }
 }
index e0356e515e8d6c4b594d45c6d79703214c84a7b9..05007b0fea1b8263279700251e2f645007167682 100644 (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>
index 05ae0a4bb1555da13fa6498fd729a9d85edd720b..e1b3b948f045add3739040bdc3cfb1363ff8a6d0 100644 (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,
diff --git a/Content.Client/UserInterface/Controls/SearchListContainer.cs b/Content.Client/UserInterface/Controls/SearchListContainer.cs
new file mode 100644 (file)
index 0000000..603d7f1
--- /dev/null
@@ -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);
+    }
+}
index cccd9201a292443fe86c55612c825cf526693fd7..a7397aff38d81a55c989f23fe65b3d405e7685a1 100644 (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)
index 94d3328bde2cb28cf22c57f52da81727f9042414..474af89c268c17d5e81d93b75d5b28bba952ab99 100644 (file)
@@ -12,3 +12,5 @@ bwoink-system-typing-indicator = {$players} {$count ->
 } typing...
 
 admin-bwoink-play-sound = Bwoink?
+
+bwoink-title-none-selected = None selected
index e0dd7a03b15821264a902d4dbc4fe49021a062a5..f9f9f6b5c9fffb0f9be99761af359a274456fc82 100644 (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