Files
tbd-station-14/Content.Client/Alerts/UI/AlertsUI.xaml.cs

345 lines
12 KiB
C#

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<ClientAlertsSystem>(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);
}
}
/// <inheritdoc />
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<AlertKey, AlertState> 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
}
/// <summary>
/// This is the frame of vertical set of alerts that show up on the HUD.
/// </summary>
public interface IAlertsFrameView : IDisposable
{
event EventHandler<AlertType>? AlertPressed;
void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
IReadOnlyDictionary<AlertKey, AlertState> alertStates);
void ClearAllControls();
}
/// <summary>
/// The status effects display on the right side of the screen.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class AlertsUI : Control, IAlertsFrameView
{
// also known as Control.Children?
private readonly Dictionary<AlertKey, AlertControl> _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<AlertKey, AlertState> 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<AlertType>? 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<AlertKey, AlertState> alertStates)
{
var toRemove = new List<AlertKey>();
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<AlertKey, AlertState> 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);
}
}