diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml index fb68e6c790..ea89916ba8 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml @@ -1,15 +1,21 @@  + xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:ot="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab" + xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"> + + - - - - + + + + + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index a5c3008436..90559707f9 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -1,5 +1,7 @@ using Content.Client.Station; +using Content.Client.UserInterface.Controls; using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; using Robust.Shared.Map.Components; @@ -10,20 +12,20 @@ namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] public sealed partial class ObjectsTab : Control { - [Dependency] private readonly EntityManager _entityManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IGameTiming _timing = default!; private readonly List _objects = new(); - private List _selections = new(); + private readonly List _selections = new(); + private bool _ascending = false; // Set to false for descending order by default + private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName; + private readonly Color _altColor = Color.FromHex("#292B38"); + private readonly Color _defaultColor = Color.FromHex("#2F2F3B"); - public event Action? OnEntryKeyBindDown; + public event Action? OnEntryKeyBindDown; - // Listen I could either have like 4 different event subscribers (for map / grid / station changes) and manage their lifetimes in AdminUIController - // OR - // I can do this. - private TimeSpan _updateFrequency = TimeSpan.FromSeconds(2); - - private TimeSpan _nextUpdate = TimeSpan.FromSeconds(2); + private readonly TimeSpan _updateFrequency = TimeSpan.FromSeconds(2); + private TimeSpan _nextUpdate; public ObjectsTab() { @@ -42,6 +44,30 @@ public sealed partial class ObjectsTab : Control ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!); } + ListHeader.OnHeaderClicked += HeaderClicked; + SearchList.SearchBar = SearchLineEdit; + SearchList.GenerateItem += GenerateButton; + SearchList.DataFilterCondition += DataFilterCondition; + + RefreshObjectList(); + // Set initial selection and refresh the list to apply the initial sort order + var defaultSelection = ObjectsTabSelection.Grids; + ObjectTypeOptions.SelectId((int)defaultSelection); // Set the default selection + RefreshObjectList(defaultSelection); // Refresh the list with the default selection + + // Initialize the next update time + _nextUpdate = TimeSpan.Zero; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_timing.CurTime < _nextUpdate) + return; + + _nextUpdate = _timing.CurTime + _updateFrequency; + RefreshObjectList(); } @@ -81,32 +107,72 @@ public sealed partial class ObjectsTab : Control throw new ArgumentOutOfRangeException(nameof(selection), selection, null); } - foreach (var control in _objects) + entities.Sort((a, b) => { - ObjectList.RemoveChild(control); + var valueA = GetComparableValue(a, _headerClicked); + var valueB = GetComparableValue(b, _headerClicked); + return _ascending ? Comparer.Default.Compare(valueA, valueB) : Comparer.Default.Compare(valueB, valueA); + }); + + var listData = new List(); + for (int index = 0; index < entities.Count; index++) + { + var info = entities[index]; + listData.Add(new ObjectsListData(info, $"{info.Name} {info.Entity}", index % 2 == 0 ? _altColor : _defaultColor)); } - _objects.Clear(); - - foreach (var (name, nent) in entities) - { - var ctrl = new ObjectsTabEntry(name, nent); - _objects.Add(ctrl); - ObjectList.AddChild(ctrl); - ctrl.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(ctrl, args); - } + SearchList.PopulateList(listData); } - protected override void FrameUpdate(FrameEventArgs args) + private void GenerateButton(ListData data, ListContainerButton button) { - base.FrameUpdate(args); - - if (_timing.CurTime < _nextUpdate) + if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor }) return; - // I do not care for precision. - _nextUpdate = _timing.CurTime + _updateFrequency; + var entry = new ObjectsTabEntry(info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor }); + button.ToolTip = $"{info.Name}, {info.Entity}"; + // Add key binding event handler + entry.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(args, data); + + button.AddChild(entry); + } + + private bool DataFilterCondition(string filter, ListData listData) + { + if (listData is not ObjectsListData { FilteringString: var filteringString }) + return false; + + // If the filter is empty, do not filter out any entries + if (string.IsNullOrEmpty(filter)) + return true; + + return filteringString.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } + + private object GetComparableValue((string Name, NetEntity Entity) entity, ObjectsTabHeader.Header header) + { + return header switch + { + ObjectsTabHeader.Header.ObjectName => entity.Name, + ObjectsTabHeader.Header.EntityID => entity.Entity.ToString(), + _ => entity.Name + }; + } + + private void HeaderClicked(ObjectsTabHeader.Header header) + { + if (_headerClicked == header) + { + _ascending = !_ascending; + } + else + { + _headerClicked = header; + _ascending = true; + } + + ListHeader.UpdateHeaderSymbols(_headerClicked, _ascending); RefreshObjectList(); } @@ -118,3 +184,4 @@ public sealed partial class ObjectsTab : Control } } +public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor) : ListData; diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml index 0f6975e365..83c4cc5697 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml @@ -1,6 +1,6 @@ - - + @@ -14,4 +14,4 @@ HorizontalExpand="True" ClipText="True"/> - + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs index c9b2cd8b57..aab06c6ccd 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml.cs @@ -1,19 +1,21 @@ using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; namespace Content.Client.Administration.UI.Tabs.ObjectsTab; [GenerateTypedNameReferences] -public sealed partial class ObjectsTabEntry : ContainerButton +public sealed partial class ObjectsTabEntry : PanelContainer { public NetEntity AssocEntity; - public ObjectsTabEntry(string name, NetEntity nent) + public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox) { RobustXamlLoader.Load(this); AssocEntity = nent; EIDLabel.Text = nent.ToString(); NameLabel.Text = name; + BackgroundColorPanel.PanelOverride = styleBox; } } diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml new file mode 100644 index 0000000000..71a1f5c7bc --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml @@ -0,0 +1,21 @@ + + + + + diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs new file mode 100644 index 0000000000..3a91b5b948 --- /dev/null +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabHeader.xaml.cs @@ -0,0 +1,86 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Input; + +namespace Content.Client.Administration.UI.Tabs.ObjectsTab +{ + [GenerateTypedNameReferences] + public sealed partial class ObjectsTabHeader : Control + { + public event Action
? OnHeaderClicked; + + private const string ArrowUp = "↑"; + private const string ArrowDown = "↓"; + + public ObjectsTabHeader() + { + RobustXamlLoader.Load(this); + + ObjectNameLabel.OnKeyBindDown += ObjectNameClicked; + EntityIDLabel.OnKeyBindDown += EntityIDClicked; + } + + public Label GetHeader(Header header) + { + return header switch + { + Header.ObjectName => ObjectNameLabel, + Header.EntityID => EntityIDLabel, + _ => throw new ArgumentOutOfRangeException(nameof(header), header, null) + }; + } + + public void ResetHeaderText() + { + ObjectNameLabel.Text = Loc.GetString("object-tab-object-name"); + EntityIDLabel.Text = Loc.GetString("object-tab-entity-id"); + } + + public void UpdateHeaderSymbols(Header headerClicked, bool ascending) + { + ResetHeaderText(); + var arrow = ascending ? ArrowUp : ArrowDown; + GetHeader(headerClicked).Text += $" {arrow}"; + } + + private void HeaderClicked(GUIBoundKeyEventArgs args, Header header) + { + if (args.Function != EngineKeyFunctions.UIClick) + { + return; + } + + OnHeaderClicked?.Invoke(header); + args.Handle(); + } + + private void ObjectNameClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.ObjectName); + } + + private void EntityIDClicked(GUIBoundKeyEventArgs args) + { + HeaderClicked(args, Header.EntityID); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + ObjectNameLabel.OnKeyBindDown -= ObjectNameClicked; + EntityIDLabel.OnKeyBindDown -= EntityIDClicked; + } + } + + public enum Header + { + ObjectName, + EntityID + } + } +} diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs index a8bfaddecf..826945e7cc 100644 --- a/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PlayerTab/PlayerTab.xaml.cs @@ -53,6 +53,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data); RefreshPlayerList(_adminSystem.PlayerList); + } #region Antag Overlay @@ -110,7 +111,9 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab _players = players; PlayerCount.Text = $"Players: {_playerMan.PlayerCount}"; - var sortedPlayers = new List(players); + var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList(); + + var sortedPlayers = new List(filteredPlayers); sortedPlayers.Sort(Compare); UpdateHeaderSymbols(); diff --git a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs index a7397aff38..3d8235591a 100644 --- a/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs +++ b/Content.Client/UserInterface/Systems/Admin/AdminUIController.cs @@ -198,9 +198,12 @@ public sealed class AdminUIController : UIController, args.Handle(); } - private void ObjectsTabEntryKeyBindDown(ObjectsTabEntry entry, GUIBoundKeyEventArgs args) + private void ObjectsTabEntryKeyBindDown(GUIBoundKeyEventArgs args, ListData? data) { - var uid = entry.AssocEntity; + if (data is not ObjectsListData { Info: var info }) + return; + + var uid = info.Entity; var function = args.Function; if (function == EngineKeyFunctions.UIClick) diff --git a/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl b/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl new file mode 100644 index 0000000000..0288efcaa4 --- /dev/null +++ b/Resources/Locale/en-US/administration/ui/tabs/object-tab.ftl @@ -0,0 +1,2 @@ +object-tab-entity-id = Entity ID +object-tab-object-name = Object name