using System; using System.Collections.Generic; using Content.Client.Chat.Managers; using Content.Client.Chat.UI; using Content.Shared.Alert; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; namespace Content.Client.Alerts.UI; public sealed class AlertsFramePresenter : IDisposable { [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; private IAlertsFrameView _alertsFrame; private ClientAlertsSystem? _alertsSystem; public AlertsFramePresenter() { // This is a lot easier than a factory IoCManager.InjectDependencies(this); _alertsFrame = new AlertsUI(_chatManager); _userInterfaceManager.StateRoot.AddChild((AlertsUI) _alertsFrame); // This is required so that if we load after the system is initialized, we can bind to it immediately if (_systemManager.TryGetEntitySystem(out var alertsSystem)) SystemBindingChanged(alertsSystem); _systemManager.SystemLoaded += OnSystemLoaded; _systemManager.SystemUnloaded += OnSystemUnloaded; _alertsFrame.AlertPressed += OnAlertPressed; // initially populate the frame if system is available var alerts = alertsSystem?.ActiveAlerts; if (alerts != null) { SystemOnSyncAlerts(alertsSystem, alerts); } } /// public void Dispose() { _userInterfaceManager.StateRoot.RemoveChild((AlertsUI) _alertsFrame); _alertsFrame.Dispose(); _alertsFrame = null!; SystemBindingChanged(null); _systemManager.SystemLoaded -= OnSystemLoaded; _systemManager.SystemUnloaded -= OnSystemUnloaded; } private void OnAlertPressed(object? sender, AlertType e) { _alertsSystem?.AlertClicked(e); } private void SystemOnClearAlerts(object? sender, EventArgs e) { _alertsFrame.ClearAllControls(); } private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary e) { if (sender is ClientAlertsSystem system) _alertsFrame.SyncControls(system, system.AlertOrder, e); } //TODO: This system binding boilerplate seems to be duplicated between every presenter // prob want to pull it out into a generic object with callbacks for Onbind/OnUnbind #region System Binding private void OnSystemLoaded(object? sender, SystemChangedArgs args) { if (args.System is ClientAlertsSystem system) SystemBindingChanged(system); } private void OnSystemUnloaded(object? sender, SystemChangedArgs args) { if (args.System is ClientAlertsSystem) SystemBindingChanged(null); } private void SystemBindingChanged(ClientAlertsSystem? newSystem) { if (newSystem is null) { if (_alertsSystem is null) return; UnbindFromSystem(); } else { if (_alertsSystem is null) { BindToSystem(newSystem); return; } UnbindFromSystem(); BindToSystem(newSystem); } } private void BindToSystem(ClientAlertsSystem system) { _alertsSystem = system; system.SyncAlerts += SystemOnSyncAlerts; system.ClearAlerts += SystemOnClearAlerts; } private void UnbindFromSystem() { var system = _alertsSystem; if (system is null) throw new InvalidOperationException(); system.SyncAlerts -= SystemOnSyncAlerts; system.ClearAlerts -= SystemOnClearAlerts; } #endregion } /// /// This is the frame of vertical set of alerts that show up on the HUD. /// public interface IAlertsFrameView : IDisposable { event EventHandler? AlertPressed; void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, IReadOnlyDictionary alertStates); void ClearAllControls(); } /// /// The status effects display on the right side of the screen. /// [GenerateTypedNameReferences] public sealed partial class AlertsUI : Control, IAlertsFrameView { // also known as Control.Children? private readonly Dictionary _alertControls = new(); public AlertsUI(IChatManager chatManager) { _chatManager = chatManager; RobustXamlLoader.Load(this); LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin); LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End); LayoutContainer.SetAnchorTop(this, 0f); LayoutContainer.SetAnchorRight(this, 1f); LayoutContainer.SetAnchorBottom(this, 1f); LayoutContainer.SetMarginBottom(this, -180); LayoutContainer.SetMarginTop(this, 250); LayoutContainer.SetMarginRight(this, -10); } public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, IReadOnlyDictionary alertStates) { // remove any controls with keys no longer present if (SyncRemoveControls(alertStates)) return; // now we know that alertControls contains alerts that should still exist but // may need to updated, // also there may be some new alerts we need to show. // further, we need to ensure they are ordered w.r.t their configured order SyncUpdateControls(alertsSystem, alertOrderPrototype, alertStates); } public void ClearAllControls() { foreach (var alertControl in _alertControls.Values) { alertControl.OnPressed -= AlertControlPressed; alertControl.Dispose(); } _alertControls.Clear(); } public event EventHandler? AlertPressed; //TODO: This control caring about it's layout relative to other controls in the tree is terrible // the presenters or gamescreen should be dealing with this // probably want to tackle this after chatbox gets MVP'd #region Spaghetti public const float ChatSeparation = 38f; private readonly IChatManager _chatManager; protected override void EnteredTree() { base.EnteredTree(); _chatManager.OnChatBoxResized += OnChatResized; OnChatResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom)); } protected override void ExitedTree() { base.ExitedTree(); _chatManager.OnChatBoxResized -= OnChatResized; } private void OnChatResized(ChatResizedEventArgs chatResizedEventArgs) { // resize us to fit just below the chat box if (_chatManager.CurrentChatBox != null) LayoutContainer.SetMarginTop(this, chatResizedEventArgs.NewBottom + ChatSeparation); else LayoutContainer.SetMarginTop(this, 250); } #endregion // This makes no sense but I'm leaving it in place in case I break anything by removing it. protected override void Resized() { // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done, // this is here because there isn't currently a good way to allow the grid to adjust its height based // on constraints, otherwise we would use anchors to lay it out base.Resized(); AlertContainer.MaxGridHeight = Height; } protected override void UIScaleChanged() { AlertContainer.MaxGridHeight = Height; base.UIScaleChanged(); } private bool SyncRemoveControls(IReadOnlyDictionary alertStates) { var toRemove = new List(); foreach (var existingKey in _alertControls.Keys) { if (!alertStates.ContainsKey(existingKey)) toRemove.Add(existingKey); } foreach (var alertKeyToRemove in toRemove) { _alertControls.Remove(alertKeyToRemove, out var control); if (control == null) return true; AlertContainer.Children.Remove(control); } return false; } private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, IReadOnlyDictionary alertStates) { foreach (var (alertKey, alertState) in alertStates) { if (!alertKey.AlertType.HasValue) { Logger.WarningS("alert", "found alertkey without alerttype," + " alert keys should never be stored without an alerttype set: {0}", alertKey); continue; } var alertType = alertKey.AlertType.Value; if (!alertsSystem.TryGet(alertType, out var newAlert)) { Logger.ErrorS("alert", "Unrecognized alertType {0}", alertType); continue; } if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) && existingAlertControl.Alert.AlertType == newAlert.AlertType) { // key is the same, simply update the existing control severity / cooldown existingAlertControl.SetSeverity(alertState.Severity); existingAlertControl.Cooldown = alertState.Cooldown; } else { if (existingAlertControl != null) AlertContainer.Children.Remove(existingAlertControl); // this is a new alert + alert key or just a different alert with the same // key, create the control and add it in the appropriate order var newAlertControl = CreateAlertControl(newAlert, alertState); //TODO: Can the presenter sort the states before giving it to us? if (alertOrderPrototype != null) { var added = false; foreach (var alertControl in AlertContainer.Children) { if (alertOrderPrototype.Compare(newAlert, ((AlertControl) alertControl).Alert) >= 0) continue; var idx = alertControl.GetPositionInParent(); AlertContainer.Children.Add(newAlertControl); newAlertControl.SetPositionInParent(idx); added = true; break; } if (!added) AlertContainer.Children.Add(newAlertControl); } else AlertContainer.Children.Add(newAlertControl); _alertControls[newAlert.AlertKey] = newAlertControl; } } } private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState) { var alertControl = new AlertControl(alert, alertState.Severity) { Cooldown = alertState.Cooldown }; alertControl.OnPressed += AlertControlPressed; return alertControl; } private void AlertControlPressed(BaseButton.ButtonEventArgs args) { if (args.Button is not AlertControl control) return; if (args.Event.Function != EngineKeyFunctions.UIClick) return; AlertPressed?.Invoke(this, control.Alert.AlertType); } }