Hud refactor (#7202)
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: Jezithyr <jmaster9999@gmail.com> Co-authored-by: Jezithyr <Jezithyr@gmail.com> Co-authored-by: Visne <39844191+Visne@users.noreply.github.com> Co-authored-by: wrexbe <wrexbe@protonmail.com> Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
This commit is contained in:
106
Content.Client/UserInterface/BoundKeyHelpers.cs
Normal file
106
Content.Client/UserInterface/BoundKeyHelpers.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
public static class BoundKeyHelper
|
||||
{
|
||||
public static string ShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
// need to use shortened key names so they fit in the buttons.
|
||||
return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " ";
|
||||
}
|
||||
|
||||
private static string? DefaultShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
var name = FormattedMessage.EscapeText(IoCManager.Resolve<IInputManager>().GetKeyFunctionButtonString(keyFunction));
|
||||
return name.Length > 3 ? null : name;
|
||||
}
|
||||
|
||||
public static bool TryGetShortKeyName(BoundKeyFunction keyFunction, [NotNullWhen(true)] out string? name)
|
||||
{
|
||||
if (IoCManager.Resolve<IInputManager>().TryGetKeyBinding(keyFunction, out var binding))
|
||||
{
|
||||
// can't possibly fit a modifier key in the top button, so omit it
|
||||
var key = binding.BaseKey;
|
||||
if (binding.Mod1 != Keyboard.Key.Unknown || binding.Mod2 != Keyboard.Key.Unknown ||
|
||||
binding.Mod3 != Keyboard.Key.Unknown)
|
||||
{
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
name = null;
|
||||
name = key switch
|
||||
{
|
||||
Keyboard.Key.Apostrophe => "'",
|
||||
Keyboard.Key.Comma => ",",
|
||||
Keyboard.Key.Delete => "Del",
|
||||
Keyboard.Key.Down => "Dwn",
|
||||
Keyboard.Key.Escape => "Esc",
|
||||
Keyboard.Key.Equal => "=",
|
||||
Keyboard.Key.Home => "Hom",
|
||||
Keyboard.Key.Insert => "Ins",
|
||||
Keyboard.Key.Left => "Lft",
|
||||
Keyboard.Key.Menu => "Men",
|
||||
Keyboard.Key.Minus => "-",
|
||||
Keyboard.Key.Num0 => "0",
|
||||
Keyboard.Key.Num1 => "1",
|
||||
Keyboard.Key.Num2 => "2",
|
||||
Keyboard.Key.Num3 => "3",
|
||||
Keyboard.Key.Num4 => "4",
|
||||
Keyboard.Key.Num5 => "5",
|
||||
Keyboard.Key.Num6 => "6",
|
||||
Keyboard.Key.Num7 => "7",
|
||||
Keyboard.Key.Num8 => "8",
|
||||
Keyboard.Key.Num9 => "9",
|
||||
Keyboard.Key.Pause => "||",
|
||||
Keyboard.Key.Period => ".",
|
||||
Keyboard.Key.Return => "Ret",
|
||||
Keyboard.Key.Right => "Rgt",
|
||||
Keyboard.Key.Slash => "/",
|
||||
Keyboard.Key.Space => "Spc",
|
||||
Keyboard.Key.Tab => "Tab",
|
||||
Keyboard.Key.Tilde => "~",
|
||||
Keyboard.Key.BackSlash => "\\",
|
||||
Keyboard.Key.BackSpace => "Bks",
|
||||
Keyboard.Key.LBracket => "[",
|
||||
Keyboard.Key.MouseButton4 => "M4",
|
||||
Keyboard.Key.MouseButton5 => "M5",
|
||||
Keyboard.Key.MouseButton6 => "M6",
|
||||
Keyboard.Key.MouseButton7 => "M7",
|
||||
Keyboard.Key.MouseButton8 => "M8",
|
||||
Keyboard.Key.MouseButton9 => "M9",
|
||||
Keyboard.Key.MouseLeft => "ML",
|
||||
Keyboard.Key.MouseMiddle => "MM",
|
||||
Keyboard.Key.MouseRight => "MR",
|
||||
Keyboard.Key.NumpadDecimal => "N.",
|
||||
Keyboard.Key.NumpadDivide => "N/",
|
||||
Keyboard.Key.NumpadEnter => "Ent",
|
||||
Keyboard.Key.NumpadMultiply => "*",
|
||||
Keyboard.Key.NumpadNum0 => "0",
|
||||
Keyboard.Key.NumpadNum1 => "1",
|
||||
Keyboard.Key.NumpadNum2 => "2",
|
||||
Keyboard.Key.NumpadNum3 => "3",
|
||||
Keyboard.Key.NumpadNum4 => "4",
|
||||
Keyboard.Key.NumpadNum5 => "5",
|
||||
Keyboard.Key.NumpadNum6 => "6",
|
||||
Keyboard.Key.NumpadNum7 => "7",
|
||||
Keyboard.Key.NumpadNum8 => "8",
|
||||
Keyboard.Key.NumpadNum9 => "9",
|
||||
Keyboard.Key.NumpadSubtract => "N-",
|
||||
Keyboard.Key.PageDown => "PgD",
|
||||
Keyboard.Key.PageUp => "PgU",
|
||||
Keyboard.Key.RBracket => "]",
|
||||
Keyboard.Key.SemiColon => ";",
|
||||
_ => DefaultShortKeyName(keyFunction)
|
||||
};
|
||||
return name != null;
|
||||
}
|
||||
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
144
Content.Client/UserInterface/Controls/MenuButton.cs
Normal file
144
Content.Client/UserInterface/Controls/MenuButton.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class MenuButton : ContainerButton
|
||||
{
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
public const string StyleClassLabelTopButton = "topButtonLabel";
|
||||
public const string StyleClassRedTopButton = "topButtonLabel";
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorRedNormal = Color.FromHex("#FEFEFE");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorRedHovered = Color.FromHex("#FFFFFF");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
|
||||
private const float VertPad = 8f;
|
||||
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
|
||||
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
|
||||
|
||||
private BoundKeyFunction _function;
|
||||
private readonly BoxContainer _root;
|
||||
private readonly TextureRect _buttonIcon;
|
||||
private readonly Label _buttonLabel;
|
||||
|
||||
public string AppendStyleClass { set => AddStyleClass(value); }
|
||||
public Texture? Icon { get => _buttonIcon.Texture; set => _buttonIcon.Texture = value; }
|
||||
|
||||
public BoundKeyFunction BoundKey
|
||||
{
|
||||
get => _function;
|
||||
set
|
||||
{
|
||||
_function = value;
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(value);
|
||||
}
|
||||
}
|
||||
|
||||
public BoxContainer ButtonRoot => _root;
|
||||
|
||||
public MenuButton()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
_buttonIcon = new TextureRect()
|
||||
{
|
||||
TextureScale = (0.5f, 0.5f),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(0, VertPad),
|
||||
ModulateSelfOverride = NormalColor,
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
_buttonLabel = new Label
|
||||
{
|
||||
Text = "",
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
ModulateSelfOverride = NormalColor,
|
||||
StyleClasses = {StyleClassLabelTopButton}
|
||||
};
|
||||
_root = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
_buttonIcon,
|
||||
_buttonLabel
|
||||
}
|
||||
};
|
||||
AddChild(_root);
|
||||
ToggleMode = true;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded += OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved += OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged += OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded -= OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved -= OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged -= OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnKeyBindingChanged(IKeyBinding obj)
|
||||
{
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(_function);
|
||||
}
|
||||
|
||||
private void OnKeyBindingChanged()
|
||||
{
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(_function);
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
// colors of children depend on style, so ensure we update when style is changed
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_buttonIcon == null || _buttonLabel == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_buttonIcon.ModulateSelfOverride = NormalColor;
|
||||
_buttonLabel.ModulateSelfOverride = NormalColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_buttonIcon.ModulateSelfOverride = ColorPressed;
|
||||
_buttonLabel.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_buttonIcon.ModulateSelfOverride = HoveredColor;
|
||||
_buttonLabel.ModulateSelfOverride = HoveredColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
}
|
||||
18
Content.Client/UserInterface/Controls/SlotButton.cs
Normal file
18
Content.Client/UserInterface/Controls/SlotButton.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Client.Inventory;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
public sealed class SlotButton : SlotControl
|
||||
{
|
||||
public SlotButton(){}
|
||||
|
||||
public SlotButton(ClientInventorySystem.SlotData slotData)
|
||||
{
|
||||
ButtonTexturePath = slotData.TextureName;
|
||||
Blocked = slotData.Blocked;
|
||||
Highlight = slotData.Highlighted;
|
||||
StorageTexturePath = "Slots/back";
|
||||
SlotName = slotData.SlotName;
|
||||
}
|
||||
}
|
||||
}
|
||||
236
Content.Client/UserInterface/Controls/SlotControl.cs
Normal file
236
Content.Client/UserInterface/Controls/SlotControl.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
[Virtual]
|
||||
public abstract class SlotControl : Control
|
||||
{
|
||||
private const string HighlightShader = "SelectionOutlineInrange";
|
||||
|
||||
public TextureRect ButtonRect { get; }
|
||||
public TextureRect BlockedRect { get; }
|
||||
public TextureRect HighlightRect { get; }
|
||||
public SpriteView SpriteView { get; }
|
||||
public SpriteView HoverSpriteView { get; }
|
||||
public TextureButton StorageButton { get; }
|
||||
public CooldownGraphic CooldownDisplay { get; }
|
||||
|
||||
public EntityUid? Entity => SpriteView.Sprite?.Owner;
|
||||
|
||||
private bool _slotNameSet;
|
||||
|
||||
private string _slotName = "";
|
||||
public string SlotName
|
||||
{
|
||||
get => _slotName;
|
||||
set
|
||||
{
|
||||
//this auto registers the button with it's parent container when it's set
|
||||
if (_slotNameSet)
|
||||
{
|
||||
Logger.Warning("Tried to set slotName after init for:" + Name);
|
||||
return;
|
||||
}
|
||||
_slotNameSet = true;
|
||||
if (Parent is IItemslotUIContainer container)
|
||||
{
|
||||
container.TryRegisterButton(this, value);
|
||||
}
|
||||
Name = "SlotButton_" + value;
|
||||
_slotName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Highlight { get => HighlightRect.Visible; set => HighlightRect.Visible = value;}
|
||||
|
||||
public bool Blocked { get => BlockedRect.Visible; set => BlockedRect.Visible = value;}
|
||||
|
||||
public Texture BlockedTexture => Theme.ResolveTexture(BlockedTexturePath);
|
||||
|
||||
private string _blockedTexturePath = "";
|
||||
public string BlockedTexturePath
|
||||
{
|
||||
get => _blockedTexturePath;
|
||||
set
|
||||
{
|
||||
_blockedTexturePath = value;
|
||||
BlockedRect.Texture = Theme.ResolveTexture(_blockedTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Texture ButtonTexture => Theme.ResolveTexture(ButtonTexturePath);
|
||||
|
||||
private string _buttonTexturePath = "";
|
||||
public string ButtonTexturePath {
|
||||
get => _buttonTexturePath;
|
||||
set
|
||||
{
|
||||
_buttonTexturePath = value;
|
||||
ButtonRect.Texture = Theme.ResolveTexture(_buttonTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Texture StorageTexture => Theme.ResolveTexture(StorageTexturePath);
|
||||
|
||||
private string _storageTexturePath = "";
|
||||
public string StorageTexturePath
|
||||
{
|
||||
get => _buttonTexturePath;
|
||||
set
|
||||
{
|
||||
_storageTexturePath = value;
|
||||
StorageButton.TextureNormal = Theme.ResolveTexture(_storageTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
private string _highlightTexturePath = "";
|
||||
public string HighlightTexturePath
|
||||
{
|
||||
get => _highlightTexturePath;
|
||||
set
|
||||
{
|
||||
_highlightTexturePath = value;
|
||||
HighlightRect.Texture = Theme.ResolveTexture(_highlightTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? Pressed;
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? Unpressed;
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? StoragePressed;
|
||||
public event Action<GUIMouseHoverEventArgs, SlotControl>? Hover;
|
||||
|
||||
public bool EntityHover => HoverSpriteView.Sprite != null;
|
||||
public bool MouseIsHovering;
|
||||
|
||||
public SlotControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
Name = "SlotButton_null";
|
||||
MinSize = (64, 64);
|
||||
AddChild(ButtonRect = new TextureRect
|
||||
{
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop
|
||||
});
|
||||
AddChild(HighlightRect = new TextureRect
|
||||
{
|
||||
Visible = false,
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Ignore
|
||||
});
|
||||
|
||||
ButtonRect.OnKeyBindDown += OnButtonPressed;
|
||||
ButtonRect.OnKeyBindUp += OnButtonUnpressed;
|
||||
|
||||
AddChild(SpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(HoverSpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(StorageButton = new TextureButton
|
||||
{
|
||||
Scale = (0.75f, 0.75f),
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
StorageButton.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
{
|
||||
OnButtonPressed(args);
|
||||
}
|
||||
};
|
||||
|
||||
StorageButton.OnPressed += OnStorageButtonPressed;
|
||||
|
||||
ButtonRect.OnMouseEntered += _ =>
|
||||
{
|
||||
MouseIsHovering = true;
|
||||
};
|
||||
ButtonRect.OnMouseEntered += OnButtonHover;
|
||||
|
||||
ButtonRect.OnMouseExited += _ =>
|
||||
{
|
||||
MouseIsHovering = false;
|
||||
ClearHover();
|
||||
};
|
||||
|
||||
AddChild(CooldownDisplay = new CooldownGraphic
|
||||
{
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
AddChild(BlockedRect = new TextureRect
|
||||
{
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop,
|
||||
Visible = false
|
||||
});
|
||||
|
||||
HighlightTexturePath = "slot_highlight";
|
||||
BlockedTexturePath = "blocked";
|
||||
}
|
||||
|
||||
public void ClearHover()
|
||||
{
|
||||
if (!EntityHover)
|
||||
return;
|
||||
|
||||
var tempQualifier = HoverSpriteView.Sprite;
|
||||
if (tempQualifier != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntityManager>().DeleteEntity(tempQualifier.Owner);
|
||||
}
|
||||
|
||||
HoverSpriteView.Sprite = null;
|
||||
}
|
||||
|
||||
private void OnButtonPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
Pressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnButtonUnpressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
Unpressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnStorageButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
StoragePressed?.Invoke(args.Event, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Pressed?.Invoke(args.Event, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonHover(GUIMouseHoverEventArgs args)
|
||||
{
|
||||
Hover?.Invoke(args, this);
|
||||
}
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
StorageButton.TextureNormal = Theme.ResolveTexture(_storageTexturePath);
|
||||
ButtonRect.Texture = Theme.ResolveTexture(_buttonTexturePath);
|
||||
HighlightRect.Texture = Theme.ResolveTexture(_highlightTexturePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
Normal file
20
Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<screens:DefaultGameScreen
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:screens="clr-namespace:Content.Client.UserInterface.Screens"
|
||||
xmlns:menuBar="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
|
||||
xmlns:actions="clr-namespace:Content.Client.UserInterface.Systems.Actions.Widgets"
|
||||
xmlns:chat="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
|
||||
xmlns:alerts="clr-namespace:Content.Client.UserInterface.Systems.Alerts.Widgets"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Ghost.Widgets"
|
||||
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
|
||||
Name="DefaultHud"
|
||||
VerticalExpand="False"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center">
|
||||
<menuBar:GameTopMenuBar Name="TopBar" Access="Protected" />
|
||||
<widgets:GhostGui Name="Ghost" Access="Protected" />
|
||||
<hotbar:HotbarGui Name="Hotbar" Access="Protected" />
|
||||
<actions:ActionsBar Name="Actions" Access="Protected" />
|
||||
<chat:ResizableChatBox Name="Chat" Access="Protected" Main="True" />
|
||||
<alerts:AlertsUI Name="Alerts" Access="Protected" />
|
||||
</screens:DefaultGameScreen>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Screens;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DefaultGameScreen : UIScreen
|
||||
{
|
||||
public DefaultGameScreen()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AutoscaleMaxResolution = new Vector2i(1080, 770);
|
||||
|
||||
SetAnchorAndMarginPreset(TopBar, LayoutPreset.TopLeft, margin: 10);
|
||||
SetAnchorAndMarginPreset(Actions, LayoutPreset.BottomLeft, margin: 10);
|
||||
SetAnchorAndMarginPreset(Ghost, LayoutPreset.BottomWide, margin: 80);
|
||||
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
|
||||
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
|
||||
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);
|
||||
|
||||
Chat.OnResized += ChatOnResized;
|
||||
}
|
||||
|
||||
private void ChatOnResized()
|
||||
{
|
||||
var marginBottom = Chat.GetValue<float>(MarginBottomProperty);
|
||||
SetMarginTop(Alerts, marginBottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.Outline;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
using Content.Client.UserInterface.Systems.Actions.Widgets;
|
||||
using Content.Client.UserInterface.Systems.Actions.Windows;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.Actions.ActionsSystem;
|
||||
using static Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow;
|
||||
using static Robust.Client.UserInterface.Control;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.LineEdit;
|
||||
using static Robust.Client.UserInterface.Controls.MultiselectOptionButton<
|
||||
Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow.Filters>;
|
||||
using static Robust.Client.UserInterface.Controls.TextureRect;
|
||||
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions;
|
||||
|
||||
public sealed class ActionUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<ActionsSystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlays = default!;
|
||||
|
||||
[UISystemDependency] private readonly ActionsSystem _actionsSystem = default!;
|
||||
[UISystemDependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
|
||||
[UISystemDependency] private readonly TargetOutlineSystem _targetOutline = default!;
|
||||
|
||||
private const int DefaultPageIndex = 0;
|
||||
private ActionButtonContainer? _container;
|
||||
private readonly List<ActionPage> _pages = new();
|
||||
private int _currentPageIndex = DefaultPageIndex;
|
||||
private readonly DragDropHelper<ActionButton> _menuDragHelper;
|
||||
private readonly TextureRect _dragShadow;
|
||||
private ActionsWindow? _window;
|
||||
|
||||
private ActionsBar? _actionsBar;
|
||||
private MenuButton? _actionButton;
|
||||
private ActionPage CurrentPage => _pages[_currentPageIndex];
|
||||
|
||||
public bool IsDragging => _menuDragHelper.IsDragging;
|
||||
|
||||
/// <summary>
|
||||
/// Action slot we are currently selecting a target for.
|
||||
/// </summary>
|
||||
public ActionButton? SelectingTargetFor { get; private set; }
|
||||
|
||||
public ActionUIController()
|
||||
{
|
||||
_menuDragHelper = new DragDropHelper<ActionButton>(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag);
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64),
|
||||
MouseFilter = MouseFilterMode.Ignore
|
||||
};
|
||||
|
||||
var pageCount = ContentKeyFunctions.GetLoadoutBoundKeys().Length;
|
||||
var buttonCount = ContentKeyFunctions.GetHotbarBoundKeys().Length;
|
||||
for (var i = 0; i < pageCount; i++)
|
||||
{
|
||||
var page = new ActionPage(buttonCount);
|
||||
_pages.Add(page);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_window = UIManager.CreateWindow<ActionsWindow>();
|
||||
_actionButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().ActionButton;
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
|
||||
_window.OnClose += () => { _actionButton.Pressed = false; };
|
||||
_window.OnOpen += () => { _actionButton.Pressed = true; };
|
||||
_window.ClearButton.OnPressed += OnClearPressed;
|
||||
_window.SearchBar.OnTextChanged += OnSearchChanged;
|
||||
_window.FilterButton.OnItemSelected += OnFilterSelected;
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
|
||||
_actionsBar = UIManager.GetActiveUIWidget<ActionsBar>();
|
||||
_actionsBar.PageButtons.LeftArrow.OnPressed += OnLeftArrowPressed;
|
||||
_actionsBar.PageButtons.RightArrow.OnPressed += OnRightArrowPressed;
|
||||
_actionButton.OnPressed += ActionButtonPressed;
|
||||
_dragShadow.Orphan();
|
||||
UIManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
var builder = CommandBinds.Builder;
|
||||
var hotbarKeys = ContentKeyFunctions.GetHotbarBoundKeys();
|
||||
for (var i = 0; i < hotbarKeys.Length; i++)
|
||||
{
|
||||
var boundId = i; // This is needed, because the lambda captures it.
|
||||
var boundKey = hotbarKeys[i];
|
||||
builder = builder.Bind(boundKey, new PointerInputCmdHandler((in PointerInputCmdArgs args) =>
|
||||
{
|
||||
if (args.State != BoundKeyState.Up)
|
||||
return false;
|
||||
|
||||
TriggerAction(boundId);
|
||||
return true;
|
||||
}, false));
|
||||
}
|
||||
|
||||
var loadoutKeys = ContentKeyFunctions.GetLoadoutBoundKeys();
|
||||
for (var i = 0; i < loadoutKeys.Length; i++)
|
||||
{
|
||||
var boundId = i; // This is needed, because the lambda captures it.
|
||||
var boundKey = loadoutKeys[i];
|
||||
builder = builder.Bind(boundKey,
|
||||
InputCmdHandler.FromDelegate(_ => ChangePage(boundId)));
|
||||
}
|
||||
|
||||
builder
|
||||
.Bind(ContentKeyFunctions.OpenActionsMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<ActionUIController>();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
if (_actionsBar != null)
|
||||
{
|
||||
_actionsBar.PageButtons.LeftArrow.OnPressed -= OnLeftArrowPressed;
|
||||
_actionsBar.PageButtons.RightArrow.OnPressed -= OnRightArrowPressed;
|
||||
}
|
||||
|
||||
if (_actionButton != null)
|
||||
{
|
||||
_actionButton.OnPressed -= ActionButtonPressed;
|
||||
_actionButton.Pressed = false;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<ActionUIController>();
|
||||
}
|
||||
|
||||
private void TriggerAction(int index)
|
||||
{
|
||||
if (CurrentPage[index] is not { } type)
|
||||
return;
|
||||
|
||||
_actionsSystem.TriggerAction(type);
|
||||
}
|
||||
|
||||
private void ChangePage(int index)
|
||||
{
|
||||
var lastPage = _pages.Count - 1;
|
||||
if (index < 0)
|
||||
{
|
||||
index = lastPage;
|
||||
}
|
||||
else if (index > lastPage)
|
||||
{
|
||||
index = 0;
|
||||
}
|
||||
|
||||
_currentPageIndex = index;
|
||||
var page = _pages[_currentPageIndex];
|
||||
_container?.SetActionData(page);
|
||||
|
||||
_actionsBar!.PageButtons.Label.Text = $"{_currentPageIndex + 1}";
|
||||
}
|
||||
|
||||
private void OnLeftArrowPressed(ButtonEventArgs args)
|
||||
{
|
||||
ChangePage(_currentPageIndex - 1);
|
||||
}
|
||||
|
||||
private void OnRightArrowPressed(ButtonEventArgs args)
|
||||
{
|
||||
ChangePage(_currentPageIndex + 1);
|
||||
}
|
||||
|
||||
private void ActionButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void ToggleWindow()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_window.Open();
|
||||
}
|
||||
|
||||
private void UpdateFilterLabel()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (_window.FilterButton.SelectedKeys.Count == 0)
|
||||
{
|
||||
_window.FilterLabel.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.FilterLabel.Visible = true;
|
||||
_window.FilterLabel.Text = Loc.GetString("ui-actionmenu-filter-label",
|
||||
("selectedLabels", string.Join(", ", _window.FilterButton.SelectedLabels)));
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesFilter(ActionType action, Filters filter)
|
||||
{
|
||||
return filter switch
|
||||
{
|
||||
Filters.Enabled => action.Enabled,
|
||||
Filters.Item => action.Provider != null && action.Provider != _actionsSystem.PlayerActions?.Owner,
|
||||
Filters.Innate => action.Provider == null || action.Provider == _actionsSystem.PlayerActions?.Owner,
|
||||
Filters.Instant => action is InstantAction,
|
||||
Filters.Targeted => action is TargetedAction,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearList()
|
||||
{
|
||||
_window?.ResultsGrid.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void PopulateActions(IEnumerable<ActionType> actions)
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
ClearList();
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var button = new ActionButton {Locked = true};
|
||||
|
||||
button.UpdateData(_entities, action);
|
||||
button.ActionPressed += OnWindowActionPressed;
|
||||
button.ActionUnpressed += OnWindowActionUnPressed;
|
||||
button.ActionFocusExited += OnWindowActionFocusExisted;
|
||||
|
||||
_window.ResultsGrid.AddChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchAndDisplay()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
var search = _window.SearchBar.Text;
|
||||
var filters = _window.FilterButton.SelectedKeys;
|
||||
|
||||
IEnumerable<ActionType>? actions = _actionsSystem.PlayerActions?.Actions;
|
||||
actions ??= Array.Empty<ActionType>();
|
||||
|
||||
if (filters.Count == 0 && string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
PopulateActions(actions);
|
||||
return;
|
||||
}
|
||||
|
||||
actions = actions.Where(action =>
|
||||
{
|
||||
if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action, filter)))
|
||||
return false;
|
||||
|
||||
if (action.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
|
||||
if (action.DisplayName.Contains((string) search, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (action.Provider == null || action.Provider == _actionsSystem.PlayerActions?.Owner)
|
||||
return false;
|
||||
|
||||
var name = _entities.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
|
||||
return name.Contains((string) search, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
PopulateActions(actions);
|
||||
}
|
||||
|
||||
private void SetAction(ActionButton button, ActionType? type)
|
||||
{
|
||||
int position;
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
button.ClearData();
|
||||
if (_container?.TryGetButtonIndex(button, out position) ?? false)
|
||||
{
|
||||
CurrentPage[position] = type;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.TryReplaceWith(_entities, type) &&
|
||||
_container != null &&
|
||||
_container.TryGetButtonIndex(button, out position))
|
||||
{
|
||||
CurrentPage[position] = type;
|
||||
}
|
||||
}
|
||||
|
||||
private void DragAction()
|
||||
{
|
||||
if (UIManager.CurrentlyHovered is ActionButton button)
|
||||
{
|
||||
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.Action is not { } type)
|
||||
{
|
||||
_menuDragHelper.EndDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
SetAction(button, type);
|
||||
}
|
||||
|
||||
if (_menuDragHelper.Dragged is {Parent: ActionButtonContainer} old)
|
||||
{
|
||||
SetAction(old, null);
|
||||
}
|
||||
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnClearPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
_window.SearchBar.Clear();
|
||||
_window.FilterButton.DeselectAll();
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnSearchChanged(LineEditEventArgs args)
|
||||
{
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnFilterSelected(ItemPressedEventArgs args)
|
||||
{
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnWindowActionPressed(GUIBoundKeyEventArgs args, ActionButton action)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
_menuDragHelper.MouseDown(action);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
DragAction();
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private void OnWindowActionFocusExisted(ActionButton button)
|
||||
{
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_menuDragHelper.MouseDown(button);
|
||||
args.Handle();
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
{
|
||||
SetAction(button, null);
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
if (UIManager.CurrentlyHovered == button)
|
||||
{
|
||||
if (button.Action is not InstantAction)
|
||||
{
|
||||
// for target actions, we go into "select target" mode, we don't
|
||||
// message the server until we actually pick our target.
|
||||
|
||||
// if we're clicking the same thing we're already targeting for, then we simply cancel
|
||||
// targeting
|
||||
ToggleTargeting(button);
|
||||
return;
|
||||
}
|
||||
|
||||
_actionsSystem.TriggerAction(button.Action);
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
else
|
||||
{
|
||||
DragAction();
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private bool OnMenuBeginDrag()
|
||||
{
|
||||
_dragShadow.Texture = _menuDragHelper.Dragged?.IconTexture;
|
||||
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - (32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnMenuContinueDrag(float frameTime)
|
||||
{
|
||||
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - (32, 32));
|
||||
_dragShadow.Visible = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnMenuEndDrag()
|
||||
{
|
||||
_dragShadow.Visible = false;
|
||||
}
|
||||
|
||||
public void RegisterActionContainer(ActionButtonContainer container)
|
||||
{
|
||||
if (_container != null)
|
||||
{
|
||||
Logger.Warning("Action container already defined for UI controller");
|
||||
return;
|
||||
}
|
||||
|
||||
_container = container;
|
||||
_container.ActionPressed += OnActionPressed;
|
||||
_container.ActionUnpressed += OnActionUnpressed;
|
||||
}
|
||||
|
||||
public void ClearActions()
|
||||
{
|
||||
_container?.ClearActionData();
|
||||
}
|
||||
|
||||
private void AssignSlots(List<SlotAssignment> assignments)
|
||||
{
|
||||
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
|
||||
{
|
||||
_pages[assignment.Hotbar][assignment.Slot] = assignment.Action;
|
||||
}
|
||||
|
||||
_container?.SetActionData(_pages[_currentPageIndex]);
|
||||
}
|
||||
|
||||
public void RemoveActionContainer()
|
||||
{
|
||||
_container = null;
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(ActionsSystem system)
|
||||
{
|
||||
_actionsSystem.OnLinkActions += OnComponentLinked;
|
||||
_actionsSystem.OnUnlinkActions += OnComponentUnlinked;
|
||||
_actionsSystem.ClearAssignments += ClearActions;
|
||||
_actionsSystem.AssignSlot += AssignSlots;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(ActionsSystem system)
|
||||
{
|
||||
_actionsSystem.OnLinkActions -= OnComponentLinked;
|
||||
_actionsSystem.OnUnlinkActions -= OnComponentUnlinked;
|
||||
_actionsSystem.ClearAssignments -= ClearActions;
|
||||
_actionsSystem.AssignSlot -= AssignSlots;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
_menuDragHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
|
||||
private void OnComponentLinked(ActionsComponent component)
|
||||
{
|
||||
LoadDefaultActions(component);
|
||||
_container?.SetActionData(_pages[DefaultPageIndex]);
|
||||
}
|
||||
|
||||
private void OnComponentUnlinked()
|
||||
{
|
||||
_container?.ClearActionData();
|
||||
//TODO: Clear button data
|
||||
}
|
||||
|
||||
private void LoadDefaultActions(ActionsComponent component)
|
||||
{
|
||||
var actions = component.Actions.Where(actionType => actionType.AutoPopulate).ToList();
|
||||
|
||||
var offset = 0;
|
||||
var totalPages = _pages.Count;
|
||||
var pagesLeft = totalPages;
|
||||
var currentPage = DefaultPageIndex;
|
||||
while (pagesLeft > 0)
|
||||
{
|
||||
var page = _pages[currentPage];
|
||||
var pageSize = page.Size;
|
||||
|
||||
for (var slot = 0; slot < pageSize; slot++)
|
||||
{
|
||||
var actionIndex = slot + offset;
|
||||
if (actionIndex < actions.Count)
|
||||
{
|
||||
page[slot] = actions[slot + offset];
|
||||
}
|
||||
else
|
||||
{
|
||||
page[slot] = null;
|
||||
}
|
||||
}
|
||||
|
||||
offset += pageSize;
|
||||
currentPage++;
|
||||
if (currentPage == totalPages)
|
||||
{
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
pagesLeft--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If currently targeting with this slot, stops targeting.
|
||||
/// If currently targeting with no slot or a different slot, switches to
|
||||
/// targeting with the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot"></param>
|
||||
public void ToggleTargeting(ActionButton slot)
|
||||
{
|
||||
if (SelectingTargetFor == slot)
|
||||
{
|
||||
StopTargeting();
|
||||
return;
|
||||
}
|
||||
|
||||
StartTargeting(slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts us in targeting mode, where we need to pick either a target point or entity
|
||||
/// </summary>
|
||||
private void StartTargeting(ActionButton actionSlot)
|
||||
{
|
||||
if (actionSlot.Action == null)
|
||||
return;
|
||||
|
||||
// If we were targeting something else we should stop
|
||||
StopTargeting();
|
||||
|
||||
SelectingTargetFor = actionSlot;
|
||||
|
||||
if (actionSlot.Action is not TargetedAction action)
|
||||
return;
|
||||
|
||||
// override "held-item" overlay
|
||||
if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
|
||||
{
|
||||
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
|
||||
{
|
||||
handOverlay.EntityOverride = action.Provider;
|
||||
}
|
||||
else if (action.Toggled && action.IconOn != null)
|
||||
handOverlay.IconOverride = action.IconOn.Frame0();
|
||||
else if (action.Icon != null)
|
||||
handOverlay.IconOverride = action.Icon.Frame0();
|
||||
}
|
||||
|
||||
// TODO: allow world-targets to check valid positions. E.g., maybe:
|
||||
// - Draw a red/green ghost entity
|
||||
// - Add a yes/no checkmark where the HandItemOverlay usually is
|
||||
|
||||
// Highlight valid entity targets
|
||||
if (action is not EntityTargetAction entityAction)
|
||||
return;
|
||||
|
||||
Func<EntityUid, bool>? predicate = null;
|
||||
|
||||
if (!entityAction.CanTargetSelf)
|
||||
predicate = e => e != entityAction.AttachedEntity;
|
||||
|
||||
var range = entityAction.CheckCanAccess ? action.Range : -1;
|
||||
|
||||
_interactionOutline.SetEnabled(false);
|
||||
_targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out of targeting mode if currently selecting target for an action
|
||||
/// </summary>
|
||||
public void StopTargeting()
|
||||
{
|
||||
if (SelectingTargetFor == null)
|
||||
return;
|
||||
|
||||
SelectingTargetFor = null;
|
||||
_targetOutline.Disable();
|
||||
_interactionOutline.SetEnabled(true);
|
||||
|
||||
if (!_overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay) || handOverlay == null)
|
||||
return;
|
||||
|
||||
handOverlay.IconOverride = null;
|
||||
handOverlay.EntityOverride = null;
|
||||
}
|
||||
|
||||
|
||||
//TODO: Serialize this shit
|
||||
private sealed class ActionPage
|
||||
{
|
||||
private readonly ActionType?[] _data;
|
||||
|
||||
public ActionPage(int size)
|
||||
{
|
||||
_data = new ActionType?[size];
|
||||
}
|
||||
|
||||
public ActionType? this[int index]
|
||||
{
|
||||
get => _data[index];
|
||||
set => _data[index] = value;
|
||||
}
|
||||
|
||||
public static implicit operator ActionType?[](ActionPage p)
|
||||
{
|
||||
return p._data.ToArray();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Array.Fill(_data, null);
|
||||
}
|
||||
|
||||
public int Size => _data.Length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
public sealed class ActionButton : Control
|
||||
{
|
||||
private ActionUIController Controller => UserInterfaceManager.GetUIController<ActionUIController>();
|
||||
private bool _beingHovered;
|
||||
private bool _depressed;
|
||||
private bool _toggled;
|
||||
|
||||
public BoundKeyFunction? KeyBind
|
||||
{
|
||||
set
|
||||
{
|
||||
_keybind = value;
|
||||
if (_keybind != null)
|
||||
{
|
||||
Label.Text = BoundKeyHelper.ShortKeyName(_keybind.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BoundKeyFunction? _keybind;
|
||||
|
||||
public readonly TextureRect Button;
|
||||
public readonly PanelContainer HighlightRect;
|
||||
public readonly TextureRect Icon;
|
||||
public readonly Label Label;
|
||||
public readonly SpriteView Sprite;
|
||||
public readonly CooldownGraphic Cooldown;
|
||||
|
||||
public Texture? IconTexture
|
||||
{
|
||||
get => Icon.Texture;
|
||||
private set => Icon.Texture = value;
|
||||
}
|
||||
|
||||
public ActionType? Action { get; private set; }
|
||||
public bool Locked { get; set; }
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionUnpressed;
|
||||
public event Action<ActionButton>? ActionFocusExited;
|
||||
|
||||
public ActionButton()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
Button = new TextureRect
|
||||
{
|
||||
Name = "Button",
|
||||
TextureScale = new Vector2(2, 2)
|
||||
};
|
||||
HighlightRect = new PanelContainer
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHandSlotHighlight},
|
||||
MinSize = (32, 32),
|
||||
Visible = false
|
||||
};
|
||||
Icon = new TextureRect
|
||||
{
|
||||
Name = "Icon",
|
||||
TextureScale = new Vector2(2, 2),
|
||||
MaxSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale
|
||||
};
|
||||
Label = new Label
|
||||
{
|
||||
Name = "Label",
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
Sprite = new SpriteView
|
||||
{
|
||||
Name = "Sprite",
|
||||
OverrideDirection = Direction.South
|
||||
};
|
||||
Cooldown = new CooldownGraphic {Visible = false};
|
||||
|
||||
AddChild(Button);
|
||||
AddChild(HighlightRect);
|
||||
AddChild(Icon);
|
||||
AddChild(Label);
|
||||
AddChild(Sprite);
|
||||
AddChild(Cooldown);
|
||||
|
||||
Button.Modulate = new Color(255, 255, 255, 150);
|
||||
Icon.Modulate = new Color(255, 255, 255, 150);
|
||||
|
||||
OnThemeUpdated();
|
||||
OnThemeUpdated();
|
||||
|
||||
OnKeyBindDown += args =>
|
||||
{
|
||||
Depress(args, true);
|
||||
OnPressed(args);
|
||||
};
|
||||
OnKeyBindUp += args =>
|
||||
{
|
||||
Depress(args, false);
|
||||
OnUnpressed(args);
|
||||
};
|
||||
|
||||
TooltipDelay = 0.5f;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
Button.Texture = Theme.ResolveTexture("SlotBackground");
|
||||
Label.FontColorOverride = Theme.ResolveColorOrSpecified("whiteText");
|
||||
}
|
||||
|
||||
private void OnPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
ActionPressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnUnpressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
ActionUnpressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
return new ActionAlertTooltip(name, decr);
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
ActionFocusExited?.Invoke(this);
|
||||
}
|
||||
|
||||
public bool TryReplaceWith(IEntityManager entityManager, ActionType action)
|
||||
{
|
||||
if (Locked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateData(entityManager, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateData(IEntityManager entityManager, ActionType action)
|
||||
{
|
||||
Action = action;
|
||||
|
||||
if (action.Icon != null)
|
||||
{
|
||||
IconTexture = GetIcon();
|
||||
Sprite.Sprite = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.Provider == null ||
|
||||
!entityManager.TryGetComponent(action.Provider.Value, out SpriteComponent? sprite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IconTexture = null;
|
||||
Sprite.Sprite = sprite;
|
||||
}
|
||||
|
||||
public void ClearData()
|
||||
{
|
||||
Action = null;
|
||||
IconTexture = null;
|
||||
Sprite.Sprite = null;
|
||||
Cooldown.Visible = false;
|
||||
Cooldown.Progress = 1;
|
||||
}
|
||||
|
||||
private Texture? GetIcon()
|
||||
{
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
return _toggled ? (Action.IconOn ?? Action.Icon)?.Frame0() : Action.Icon?.Frame0();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (Action?.Cooldown != null)
|
||||
{
|
||||
Cooldown.FromTime(Action.Cooldown.Value.Start, Action.Cooldown.Value.End);
|
||||
}
|
||||
|
||||
if (Action != null && _toggled != Action.Toggled)
|
||||
{
|
||||
_toggled = Action.Toggled;
|
||||
IconTexture = GetIcon();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void MouseEntered()
|
||||
{
|
||||
base.MouseEntered();
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
_beingHovered = false;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Press this button down. If it was depressed and now set to not depressed, will
|
||||
/// trigger the action.
|
||||
/// </summary>
|
||||
public void Depress(GUIBoundKeyEventArgs args, bool depress)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
if (Action is not {Enabled: true})
|
||||
return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
OnUnpressed(args);
|
||||
}
|
||||
|
||||
_depressed = depress;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
public void DrawModeChanged()
|
||||
{
|
||||
HighlightRect.Visible = _beingHovered;
|
||||
|
||||
// always show the normal empty button style if no action in this slot
|
||||
if (Action == null)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered && (Controller.IsDragging || Action.Enabled))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
}
|
||||
|
||||
// it's only depress-able if it's usable, so if we're depressed
|
||||
// show the depressed style
|
||||
if (_depressed)
|
||||
{
|
||||
HighlightRect.Visible = false;
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's toggled on, always show the toggled on style (currently same as depressed style)
|
||||
if (Action.Toggled || Controller.SelectingTargetFor == this)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(Action.IconOn != null
|
||||
? ContainerButton.StylePseudoClassNormal
|
||||
: ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Action.Enabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class ActionButtonContainer : GridContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionUnpressed;
|
||||
public event Action<ActionButton>? ActionFocusExited;
|
||||
|
||||
public ActionButtonContainer()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
UserInterfaceManager.GetUIController<ActionUIController>().RegisterActionContainer(this);
|
||||
}
|
||||
|
||||
public ActionButton this[int index]
|
||||
{
|
||||
get => (ActionButton) GetChild(index);
|
||||
set
|
||||
{
|
||||
AddChild(value);
|
||||
value.SetPositionInParent(index);
|
||||
value.ActionPressed += ActionPressed;
|
||||
value.ActionUnpressed += ActionUnpressed;
|
||||
value.ActionFocusExited += ActionFocusExited;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetActionData(params ActionType?[] actionTypes)
|
||||
{
|
||||
ClearActionData();
|
||||
|
||||
for (var i = 0; i < actionTypes.Length; i++)
|
||||
{
|
||||
var action = actionTypes[i];
|
||||
if (action == null)
|
||||
continue;
|
||||
|
||||
((ActionButton) GetChild(i)).UpdateData(_entityManager, action);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearActionData()
|
||||
{
|
||||
foreach (var button in Children)
|
||||
{
|
||||
((ActionButton) button).ClearData();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ChildAdded(Control newChild)
|
||||
{
|
||||
base.ChildAdded(newChild);
|
||||
|
||||
if (newChild is not ActionButton button)
|
||||
return;
|
||||
|
||||
button.ActionPressed += ActionPressed;
|
||||
button.ActionUnpressed += ActionUnpressed;
|
||||
button.ActionFocusExited += ActionFocusExited;
|
||||
}
|
||||
|
||||
public bool TryGetButtonIndex(ActionButton button, out int position)
|
||||
{
|
||||
if (button.Parent != this)
|
||||
{
|
||||
position = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
position = button.GetPositionInParent();
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<ActionButton> GetButtons()
|
||||
{
|
||||
foreach (var control in Children)
|
||||
{
|
||||
if (control is ActionButton button)
|
||||
yield return button;
|
||||
}
|
||||
}
|
||||
|
||||
~ActionButtonContainer()
|
||||
{
|
||||
UserInterfaceManager.GetUIController<ActionUIController>().RemoveActionContainer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<controls:ActionPageButtons
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
|
||||
<TextureButton TexturePath="/Textures/Interface/Nano/left_arrow.svg.192dpi.png"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Scale="0.5 0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="LeftArrow"
|
||||
Access="Public"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<Label Text="1" SizeFlagsStretchRatio="1" Name="Label" Access="Public" />
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<TextureButton TexturePath="/Textures/Interface/Nano/right_arrow.svg.192dpi.png"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Scale="0.5 0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="RightArrow"
|
||||
Access="Public"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
</controls:ActionPageButtons>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionPageButtons : Control
|
||||
{
|
||||
public ActionPageButtons()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<controls:ActionTooltip
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls"
|
||||
StyleClasses="StyleClassTooltipPanel">
|
||||
<BoxContainer Orientation="Vertical" RectClipContent="True">
|
||||
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionTitle"/>
|
||||
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionDescription"/>
|
||||
</BoxContainer>
|
||||
</controls:ActionTooltip>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionTooltip : PanelContainer
|
||||
{
|
||||
public ActionTooltip()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<widgets:ActionsBar
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:in="clr-namespace:Content.Shared.Input;assembly=Content.Shared"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Actions.Widgets"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls"
|
||||
VerticalExpand="False"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="False"
|
||||
>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<controls:ActionButtonContainer
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="ActionsContainer"
|
||||
Access="Public"/>
|
||||
<controls:ActionPageButtons Name="PageButtons" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</widgets:ActionsBar>
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionsBar : UIWidget
|
||||
{
|
||||
public ActionsBar()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var keys = ContentKeyFunctions.GetHotbarBoundKeys();
|
||||
for (var index = 1; index < keys.Length; index++)
|
||||
{
|
||||
ActionsContainer.Children.Add(MakeButton(index));
|
||||
}
|
||||
ActionsContainer.Children.Add(MakeButton(0));
|
||||
|
||||
ActionButton MakeButton(int index)
|
||||
{
|
||||
var boundKey = keys[index];
|
||||
var button = new ActionButton();
|
||||
button.KeyBind = boundKey;
|
||||
button.Label.Text = index.ToString();
|
||||
return button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<windows:ActionsWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Actions.Windows"
|
||||
Name="ActionsList"
|
||||
HorizontalExpand="True"
|
||||
Title="Actions"
|
||||
VerticalExpand="True"
|
||||
Resizable="True"
|
||||
MinHeight="300"
|
||||
MinWidth="300"
|
||||
>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Name="SearchContainer" Orientation="Horizontal">
|
||||
<LineEdit Name="SearchBar" Access="Public" StyleClasses="actionSearchBox" HorizontalExpand="True"
|
||||
PlaceHolder="{Loc ui-actionmenu-search-bar-placeholder-text}"/>
|
||||
</BoxContainer>
|
||||
<Button Name="ClearButton" Access="Public" Text="{Loc ui-actionmenu-clear-button}"/>
|
||||
<Label Name="FilterLabel" Access="Public"/>
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<GridContainer Name="ResultsGrid" Access="Public" MaxGridWidth="300"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</windows:ActionsWindow>
|
||||
@@ -0,0 +1,36 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Windows;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionsWindow : DefaultWindow
|
||||
{
|
||||
public MultiselectOptionButton<Filters> FilterButton { get; private set; }
|
||||
|
||||
public ActionsWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
SearchContainer.AddChild(FilterButton = new MultiselectOptionButton<Filters>
|
||||
{
|
||||
Label = Loc.GetString("ui-actionmenu-filter-button")
|
||||
});
|
||||
|
||||
foreach (var filter in Enum.GetValues<Filters>())
|
||||
{
|
||||
FilterButton.AddItem(filter.ToString(), filter);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Filters
|
||||
{
|
||||
Enabled,
|
||||
Item,
|
||||
Innate,
|
||||
Instant,
|
||||
Targeted
|
||||
}
|
||||
}
|
||||
114
Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
Normal file
114
Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Admin;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AdminUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IClientConGroupController _conGroups = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
|
||||
[UISystemDependency] private readonly VerbSystem _verbs = default!;
|
||||
|
||||
private AdminMenuWindow? _window;
|
||||
private MenuButton? _adminButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_window = UIManager.CreateWindow<AdminMenuWindow>();
|
||||
_adminButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().AdminButton;
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.Center);
|
||||
_window.PlayerTabControl.OnEntryPressed += PlayerTabEntryPressed;
|
||||
_window.OnOpen += () => _adminButton.Pressed = true;
|
||||
_window.OnClose += () => _adminButton.Pressed = false;
|
||||
|
||||
_admin.AdminStatusUpdated += AdminStatusUpdated;
|
||||
|
||||
_adminButton.OnPressed += AdminButtonPressed;
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenAdminMenu,
|
||||
InputCmdHandler.FromDelegate(_ => Toggle()));
|
||||
|
||||
AdminStatusUpdated();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
_admin.AdminStatusUpdated -= AdminStatusUpdated;
|
||||
|
||||
if (_adminButton != null)
|
||||
{
|
||||
_adminButton.Pressed = false;
|
||||
_adminButton.OnPressed -= AdminButtonPressed;
|
||||
_adminButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<AdminUIController>();
|
||||
}
|
||||
|
||||
private void AdminStatusUpdated()
|
||||
{
|
||||
_adminButton!.Visible = _conGroups.CanAdminMenu();
|
||||
}
|
||||
|
||||
private void AdminButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
Toggle();
|
||||
}
|
||||
|
||||
private void Toggle()
|
||||
{
|
||||
if (_window is {IsOpen: true})
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else if (_conGroups.CanAdminMenu())
|
||||
{
|
||||
_window?.Open();
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayerTabEntryPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not PlayerTabEntry button
|
||||
|| button.PlayerUid == null)
|
||||
return;
|
||||
|
||||
var uid = button.PlayerUid.Value;
|
||||
var function = args.Event.Function;
|
||||
|
||||
if (function == EngineKeyFunctions.UIClick)
|
||||
_conHost.ExecuteCommand($"vv {uid}");
|
||||
else if (function == EngineKeyFunctions.UseSecondary)
|
||||
_verbs.VerbMenu.OpenVerbMenu(uid, true);
|
||||
else
|
||||
return;
|
||||
|
||||
args.Event.Handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Client.Alerts;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Systems.Alerts.Widgets;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Alerts;
|
||||
|
||||
public sealed class AlertsUIController : UIController, IOnStateEntered<GameplayState>, IOnSystemChanged<ClientAlertsSystem>
|
||||
{
|
||||
[UISystemDependency] private readonly ClientAlertsSystem? _alertsSystem = default;
|
||||
|
||||
private AlertsUI? UI => UIManager.GetActiveUIWidgetOrNull<AlertsUI>();
|
||||
|
||||
private void OnAlertPressed(object? sender, AlertType e)
|
||||
{
|
||||
_alertsSystem?.AlertClicked(e);
|
||||
}
|
||||
|
||||
private void SystemOnClearAlerts(object? sender, EventArgs e)
|
||||
{
|
||||
UI?.ClearAllControls();
|
||||
}
|
||||
|
||||
private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary<AlertKey, AlertState> e)
|
||||
{
|
||||
if (sender is ClientAlertsSystem system)
|
||||
UI?.SyncControls(system, system.AlertOrder, e);
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(ClientAlertsSystem system)
|
||||
{
|
||||
system.SyncAlerts += SystemOnSyncAlerts;
|
||||
system.ClearAlerts += SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(ClientAlertsSystem system)
|
||||
{
|
||||
system.SyncAlerts -= SystemOnSyncAlerts;
|
||||
system.ClearAlerts -= SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
if (UI != null)
|
||||
{
|
||||
UI.AlertPressed += OnAlertPressed;
|
||||
}
|
||||
|
||||
// initially populate the frame if system is available
|
||||
var alerts = _alertsSystem?.ActiveAlerts;
|
||||
if (alerts != null)
|
||||
{
|
||||
SystemOnSyncAlerts(_alertsSystem, alerts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Alerts.Controls
|
||||
{
|
||||
public sealed class AlertControl : BaseButton
|
||||
{
|
||||
// shorter than default tooltip delay so user can more easily
|
||||
// see what alerts they have
|
||||
private const float CustomTooltipDelay = 0.5f;
|
||||
|
||||
public AlertPrototype Alert { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
|
||||
/// </summary>
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown
|
||||
{
|
||||
get => _cooldown;
|
||||
set
|
||||
{
|
||||
_cooldown = value;
|
||||
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
|
||||
{
|
||||
actionAlertTooltip.Cooldown = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (TimeSpan Start, TimeSpan End)? _cooldown;
|
||||
|
||||
private short? _severity;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly AnimatedTextureRect _icon;
|
||||
private readonly CooldownGraphic _cooldownGraphic;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an alert control reflecting the indicated alert + state
|
||||
/// </summary>
|
||||
/// <param name="alert">alert to display</param>
|
||||
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
|
||||
public AlertControl(AlertPrototype alert, short? severity)
|
||||
{
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
Alert = alert;
|
||||
_severity = severity;
|
||||
var specifier = alert.GetIcon(_severity);
|
||||
_icon = new AnimatedTextureRect
|
||||
{
|
||||
DisplayRect = {TextureScale = (2, 2)}
|
||||
};
|
||||
|
||||
_icon.SetFromSpriteSpecifier(specifier);
|
||||
|
||||
Children.Add(_icon);
|
||||
_cooldownGraphic = new CooldownGraphic();
|
||||
Children.Add(_cooldownGraphic);
|
||||
}
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
return new ActionAlertTooltip(Alert.Name, Alert.Description) {Cooldown = Cooldown};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the alert severity, changing the displayed icon
|
||||
/// </summary>
|
||||
public void SetSeverity(short? severity)
|
||||
{
|
||||
if (_severity != severity)
|
||||
{
|
||||
_severity = severity;
|
||||
_icon.SetFromSpriteSpecifier(Alert.GetIcon(_severity));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_cooldownGraphic.FromTime(Cooldown.Value.Start, Cooldown.Value.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<widgets:AlertsUI xmlns="https://spacestation14.io"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Alerts.Widgets"
|
||||
MinSize="64 64">
|
||||
<PanelContainer HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<BoxContainer Name="AlertContainer" Access="Public" Orientation="Vertical" />
|
||||
</PanelContainer>
|
||||
</widgets:AlertsUI>
|
||||
@@ -0,0 +1,154 @@
|
||||
using Content.Client.UserInterface.Systems.Alerts.Controls;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Alerts.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects display on the right side of the screen.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AlertsUI : UIWidget
|
||||
{
|
||||
// also known as Control.Children?
|
||||
private readonly Dictionary<AlertKey, AlertControl> _alertControls = new();
|
||||
|
||||
public AlertsUI()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
323
Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
Normal file
323
Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Bwoink;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AHelpUIController: UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<BwoinkSystem>
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
private BwoinkSystem? _bwoinkSystem;
|
||||
private MenuButton? _ahelpButton;
|
||||
private IAHelpUIHandler? _uiHelper;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_uiHelper == null);
|
||||
_ahelpButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().AHelpButton;
|
||||
_ahelpButton.OnPressed += AHelpButtonPressed;
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenAHelp,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<AHelpUIController>();
|
||||
}
|
||||
|
||||
private void OnAdminStatusUpdated()
|
||||
{
|
||||
if (_uiHelper is not { IsOpen: true })
|
||||
return;
|
||||
EnsureUIHelper();
|
||||
}
|
||||
|
||||
private void AHelpButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper!.ToggleWindow();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
_uiHelper?.Dispose();
|
||||
_uiHelper = null;
|
||||
CommandBinds.Unregister<AHelpUIController>();
|
||||
}
|
||||
public void OnSystemLoaded(BwoinkSystem system)
|
||||
{
|
||||
_bwoinkSystem = system;
|
||||
_bwoinkSystem.OnBwoinkTextMessageRecieved += RecievedBwoink;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(BwoinkSystem system)
|
||||
{
|
||||
_bwoinkSystem = null;
|
||||
}
|
||||
|
||||
private void SetAHelpPressed(bool pressed)
|
||||
{
|
||||
if (_ahelpButton == null || _ahelpButton.Pressed == pressed)
|
||||
return;
|
||||
_ahelpButton.StyleClasses.Remove(MenuButton.StyleClassRedTopButton);
|
||||
_ahelpButton.Pressed = pressed;
|
||||
}
|
||||
|
||||
private void RecievedBwoink(object? sender, SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
Logger.InfoS("c.s.go.es.bwoink", $"@{message.UserId}: {message.Text}");
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (localPlayer.UserId != message.TrueSender)
|
||||
{
|
||||
SoundSystem.Play("/Audio/Effects/adminhelp.ogg", Filter.Local());
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsOpen)
|
||||
{
|
||||
_ahelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
|
||||
}
|
||||
_uiHelper!.Receive(message);
|
||||
}
|
||||
|
||||
public void EnsureUIHelper()
|
||||
{
|
||||
var isAdmin = _adminManager.HasFlag(AdminFlags.Adminhelp);
|
||||
|
||||
if (_uiHelper != null && _uiHelper.IsAdmin == isAdmin)
|
||||
return;
|
||||
|
||||
_uiHelper?.Dispose();
|
||||
var ownerUserId = _playerManager!.LocalPlayer!.UserId;
|
||||
_uiHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
|
||||
|
||||
_uiHelper.SendMessageAction = (userId, textMessage) => _bwoinkSystem?.Send(userId, textMessage);
|
||||
_uiHelper.OnClose += () => { SetAHelpPressed(false); };
|
||||
_uiHelper.OnOpen += () => { SetAHelpPressed(true); };
|
||||
SetAHelpPressed(_uiHelper.IsOpen);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_uiHelper?.Close();
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EnsureUIHelper();
|
||||
if (_uiHelper!.IsOpen)
|
||||
return;
|
||||
_uiHelper!.Open(localPlayer.UserId);
|
||||
}
|
||||
public void Open(NetUserId userId)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsAdmin)
|
||||
return;
|
||||
_uiHelper?.Open(userId);
|
||||
}
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper?.ToggleWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAHelpUIHandler: IDisposable
|
||||
{
|
||||
public bool IsAdmin { get; }
|
||||
public bool IsOpen { get; }
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message);
|
||||
public void Close();
|
||||
public void Open(NetUserId netUserId);
|
||||
public void ToggleWindow();
|
||||
public event Action OnClose;
|
||||
public event Action OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
}
|
||||
public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
||||
{
|
||||
private readonly NetUserId _ownerId;
|
||||
public AdminAHelpUIHandler(NetUserId owner)
|
||||
{
|
||||
_ownerId = owner;
|
||||
}
|
||||
private readonly Dictionary<NetUserId, BwoinkPanel> _activePanelMap = new();
|
||||
public bool IsAdmin => true;
|
||||
public bool IsOpen => _window is { Disposed: false, IsOpen: true };
|
||||
private BwoinkWindow? _window;
|
||||
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
var window = EnsurePanel(message.UserId);
|
||||
window.ReceiveLine(message);
|
||||
_window?.OnBwoink(message.UserId);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsurePanel(_ownerId);
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
|
||||
public void Open(NetUserId channelId)
|
||||
{
|
||||
SelectChannel(channelId);
|
||||
_window?.OpenCentered();
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_window = new BwoinkWindow(this);
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
}
|
||||
public BwoinkPanel EnsurePanel(NetUserId channelId)
|
||||
{
|
||||
EnsureWindow();
|
||||
|
||||
if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
|
||||
return existingPanel;
|
||||
|
||||
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text));
|
||||
existingPanel.Visible = false;
|
||||
if (!_window!.BwoinkArea.Children.Contains(existingPanel))
|
||||
_window.BwoinkArea.AddChild(existingPanel);
|
||||
|
||||
return existingPanel;
|
||||
}
|
||||
public bool TryGetChannel(NetUserId ch, [NotNullWhen(true)] out BwoinkPanel? bp) => _activePanelMap.TryGetValue(ch, out bp);
|
||||
|
||||
private void SelectChannel(NetUserId uid)
|
||||
{
|
||||
EnsurePanel(uid);
|
||||
_window!.SelectChannel(uid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
_activePanelMap.Clear();
|
||||
}
|
||||
}
|
||||
public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
||||
{
|
||||
private readonly NetUserId _ownerId;
|
||||
public UserAHelpUIHandler(NetUserId owner)
|
||||
{
|
||||
_ownerId = owner;
|
||||
}
|
||||
public bool IsAdmin => false;
|
||||
public bool IsOpen => _window is { Disposed: false, IsOpen: true };
|
||||
private DefaultWindow? _window;
|
||||
private BwoinkPanel? _chatPanel;
|
||||
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
DebugTools.Assert(message.UserId == _ownerId);
|
||||
EnsureInit();
|
||||
_chatPanel!.ReceiveLine(message);
|
||||
_window!.OpenCentered();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureInit();
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
|
||||
public void Open(NetUserId channelId)
|
||||
{
|
||||
EnsureInit();
|
||||
_window!.OpenCentered();
|
||||
}
|
||||
|
||||
private void EnsureInit()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text));
|
||||
_window = new DefaultWindow()
|
||||
{
|
||||
TitleClass="windowTitleAlert",
|
||||
HeaderClass="windowHeaderAlert",
|
||||
Title=Loc.GetString("bwoink-user-title"),
|
||||
SetSize=(400, 200),
|
||||
};
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
_window.Contents.AddChild(_chatPanel);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
_chatPanel = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using Content.Client.CharacterInfo;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Character.Controls;
|
||||
using Content.Client.UserInterface.Systems.Character.Windows;
|
||||
using Content.Client.UserInterface.Systems.Objectives.Controls;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.CharacterInfo.CharacterInfoSystem;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Character;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem>
|
||||
{
|
||||
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
|
||||
|
||||
private CharacterWindow? _window;
|
||||
private MenuButton? _characterButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_characterButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().CharacterButton;
|
||||
_characterButton.OnPressed += CharacterButtonPressed;
|
||||
|
||||
_window = UIManager.CreateWindow<CharacterWindow>();
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
|
||||
|
||||
_window.OnClose += () => { _characterButton.Pressed = false; };
|
||||
_window.OnOpen += () => { _characterButton.Pressed = true; };
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenCharacterMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<CharacterUIController>();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
if (_characterButton != null)
|
||||
{
|
||||
_characterButton.OnPressed -= CharacterButtonPressed;
|
||||
_characterButton.Pressed = false;
|
||||
_characterButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<CharacterUIController>();
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate += CharacterUpdated;
|
||||
system.OnCharacterDetached += CharacterDetached;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate -= CharacterUpdated;
|
||||
system.OnCharacterDetached -= CharacterDetached;
|
||||
}
|
||||
|
||||
private void CharacterUpdated(CharacterData data)
|
||||
{
|
||||
if (_window == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (job, objectives, briefing, sprite, entityName) = data;
|
||||
|
||||
_window.SubText.Text = job;
|
||||
_window.Objectives.RemoveAllChildren();
|
||||
|
||||
foreach (var (groupId, conditions) in objectives)
|
||||
{
|
||||
var objectiveControl = new CharacterObjectiveControl
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Modulate = Color.Gray
|
||||
};
|
||||
|
||||
objectiveControl.AddChild(new Label
|
||||
{
|
||||
Text = groupId,
|
||||
Modulate = Color.LightSkyBlue
|
||||
});
|
||||
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
var conditionControl = new ObjectiveConditionsControl();
|
||||
conditionControl.ProgressTexture.Texture = condition.SpriteSpecifier.Frame0();
|
||||
conditionControl.ProgressTexture.Progress = condition.Progress;
|
||||
|
||||
conditionControl.Title.Text = condition.Title;
|
||||
conditionControl.Description.Text = condition.Description;
|
||||
|
||||
objectiveControl.AddChild(conditionControl);
|
||||
}
|
||||
|
||||
var briefingControl = new ObjectiveBriefingControl();
|
||||
briefingControl.Label.Text = briefing;
|
||||
|
||||
objectiveControl.AddChild(briefingControl);
|
||||
_window.Objectives.AddChild(objectiveControl);
|
||||
}
|
||||
|
||||
_window.SpriteView.Sprite = sprite;
|
||||
_window.NameLabel.Text = entityName;
|
||||
}
|
||||
|
||||
private void CharacterDetached()
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
|
||||
private void CharacterButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void CloseWindow()
|
||||
{
|
||||
_window!.Close();
|
||||
}
|
||||
|
||||
private void ToggleWindow()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
_characterInfo.RequestCharacterInfo();
|
||||
_window.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<controls:CharacterObjectiveControl
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Character.Controls"
|
||||
Orientation="Vertical"
|
||||
Modulate="#808080">
|
||||
<Label Name="Group" Modulate="#87CEFA" Access="Public"/>
|
||||
</controls:CharacterObjectiveControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Character.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CharacterObjectiveControl : BoxContainer
|
||||
{
|
||||
public CharacterObjectiveControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<windows:CharacterWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Character.Windows"
|
||||
Title="{Loc 'character-info-title'}"
|
||||
MinWidth="400"
|
||||
MinHeight="545">
|
||||
<ScrollContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public"/>
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
|
||||
<Label Name="NameLabel" Access="Public"/>
|
||||
<Label Name="SubText" VerticalAlignment="Top" StyleClasses="LabelSubText" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Label Text="{Loc 'character-info-objectives-label'}" HorizontalAlignment="Center"/>
|
||||
<BoxContainer Orientation="Vertical" Name="Objectives" Access="Public"/>
|
||||
<cc:Placeholder PlaceholderText="{Loc 'character-info-roles-antagonist-text'}"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</windows:CharacterWindow>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Character.Windows;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CharacterWindow : DefaultWindow
|
||||
{
|
||||
public CharacterWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
715
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Normal file
715
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Normal file
@@ -0,0 +1,715 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Chat;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.TypingIndicator;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Ghost;
|
||||
using Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat;
|
||||
|
||||
public sealed class ChatUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IChatManager _manager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
[Dependency] private readonly IClientNetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
|
||||
[UISystemDependency] private readonly ExamineSystem? _examine = default;
|
||||
[UISystemDependency] private readonly GhostSystem? _ghost = default;
|
||||
[UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public const char AliasLocal = '.';
|
||||
public const char AliasConsole = '/';
|
||||
public const char AliasDead = '\\';
|
||||
public const char AliasOOC = '[';
|
||||
public const char AliasEmotes = '@';
|
||||
public const char AliasAdmin = ']';
|
||||
public const char AliasRadio = ';';
|
||||
public const char AliasWhisper = ',';
|
||||
|
||||
private static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
|
||||
{
|
||||
{AliasLocal, ChatSelectChannel.Local},
|
||||
{AliasWhisper, ChatSelectChannel.Whisper},
|
||||
{AliasConsole, ChatSelectChannel.Console},
|
||||
{AliasOOC, ChatSelectChannel.OOC},
|
||||
{AliasEmotes, ChatSelectChannel.Emotes},
|
||||
{AliasAdmin, ChatSelectChannel.Admin},
|
||||
{AliasRadio, ChatSelectChannel.Radio},
|
||||
{AliasDead, ChatSelectChannel.Dead}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes =
|
||||
PrefixToChannel.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of chars allowed to fit in a single speech bubble.
|
||||
/// </summary>
|
||||
private const int SingleBubbleCharLimit = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Base queue delay each speech bubble has.
|
||||
/// </summary>
|
||||
private const float BubbleDelayBase = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Factor multiplied by speech bubble char length to add to delay.
|
||||
/// </summary>
|
||||
private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of speech bubbles over a single entity at once.
|
||||
/// </summary>
|
||||
private const int SpeechBubbleCap = 4;
|
||||
|
||||
private LayoutContainer _speechBubbleRoot = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Speech bubbles that are currently visible on screen.
|
||||
/// We track them to push them up when new ones get added.
|
||||
/// </summary>
|
||||
private readonly Dictionary<EntityUid, List<SpeechBubble>> _activeSpeechBubbles =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Speech bubbles that are to-be-sent because of the "rate limit" they have.
|
||||
/// </summary>
|
||||
private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
|
||||
= new();
|
||||
|
||||
private readonly HashSet<ChatBox> _chats = new();
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of characters an entity can send in one message
|
||||
/// </summary>
|
||||
public int MaxMessageLength => _config.GetCVar(CCVars.ChatMaxMessageLength);
|
||||
|
||||
/// <summary>
|
||||
/// For currently disabled chat filters,
|
||||
/// unread messages (messages received since the channel has been filtered out).
|
||||
/// </summary>
|
||||
private readonly Dictionary<ChatChannel, int> _unreadMessages = new();
|
||||
|
||||
public readonly List<StoredChatMessage> History = new();
|
||||
|
||||
// Maintains which channels a client should be able to filter (for showing in the chatbox)
|
||||
// and select (for attempting to send on).
|
||||
// This may not always actually match with what the server will actually allow them to
|
||||
// send / receive on, it is only what the user can select in the UI. For example,
|
||||
// if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
|
||||
// to the server to handle invalid attempts to use particular channels and not send messages for
|
||||
// channels the user shouldn't be able to hear.
|
||||
//
|
||||
// Note that Command is an available selection in the chatbox channel selector,
|
||||
// which is not actually a chat channel but is always available.
|
||||
public ChatSelectChannel CanSendChannels { get; private set; }
|
||||
public ChatChannel FilterableChannels { get; private set; }
|
||||
public ChatSelectChannel SelectableChannels { get; private set; }
|
||||
private ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
|
||||
|
||||
public event Action<ChatSelectChannel>? CanSendChannelsChanged;
|
||||
public event Action<ChatChannel>? FilterableChannelsChanged;
|
||||
public event Action<ChatSelectChannel>? SelectableChannelsChanged;
|
||||
public event Action<ChatChannel, int?>? UnreadMessageCountsUpdated;
|
||||
public event Action<StoredChatMessage>? MessageAdded;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("chat");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
_admin.AdminStatusUpdated += UpdateChannelPermissions;
|
||||
_player.LocalPlayerChanged += OnLocalPlayerChanged;
|
||||
_state.OnStateChanged += StateChanged;
|
||||
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
|
||||
_speechBubbleRoot = new LayoutContainer();
|
||||
|
||||
OnLocalPlayerChanged(new LocalPlayerChangedEventArgs(null, _player.LocalPlayer));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChat()));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusLocalChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Local)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusWhisperChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Whisper)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.OOC)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Admin)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusRadio,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Radio)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusDeadChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Dead)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.FocusConsoleChat,
|
||||
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Console)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
|
||||
InputCmdHandler.FromDelegate(_ => CycleChatChannel(true)));
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
|
||||
InputCmdHandler.FromDelegate(_ => CycleChatChannel(false)));
|
||||
}
|
||||
|
||||
private void FocusChat()
|
||||
{
|
||||
foreach (var chat in _chats)
|
||||
{
|
||||
if (!chat.Main)
|
||||
continue;
|
||||
|
||||
chat.Focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void FocusChannel(ChatSelectChannel channel)
|
||||
{
|
||||
foreach (var chat in _chats)
|
||||
{
|
||||
if (!chat.Main)
|
||||
continue;
|
||||
|
||||
chat.Focus(channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void CycleChatChannel(bool forward)
|
||||
{
|
||||
foreach (var chat in _chats)
|
||||
{
|
||||
if (!chat.Main)
|
||||
continue;
|
||||
|
||||
chat.CycleChatChannel(forward);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StateChanged(StateChangedEventArgs args)
|
||||
{
|
||||
if (args.NewState is GameplayState)
|
||||
{
|
||||
PreferredChannel = ChatSelectChannel.Local;
|
||||
}
|
||||
|
||||
UpdateChannelPermissions();
|
||||
|
||||
if (_speechBubbleRoot.Parent == UIManager.StateRoot)
|
||||
return;
|
||||
|
||||
_speechBubbleRoot.Orphan();
|
||||
LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide);
|
||||
UIManager.StateRoot.AddChild(_speechBubbleRoot);
|
||||
_speechBubbleRoot.SetPositionFirst();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerChanged(LocalPlayerChangedEventArgs obj)
|
||||
{
|
||||
if (obj.OldPlayer != null)
|
||||
{
|
||||
obj.OldPlayer.EntityAttached -= OnLocalPlayerEntityAttached;
|
||||
obj.OldPlayer.EntityDetached -= OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
if (obj.NewPlayer != null)
|
||||
{
|
||||
obj.NewPlayer.EntityAttached += OnLocalPlayerEntityAttached;
|
||||
obj.NewPlayer.EntityDetached += OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityAttached(EntityAttachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityDetached(EntityDetachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void AddSpeechBubble(MsgChatMessage msg, SpeechBubble.SpeechType speechType)
|
||||
{
|
||||
if (!_entities.EntityExists(msg.SenderEntity))
|
||||
{
|
||||
_sawmill.Debug("Got local chat message with invalid sender entity: {0}", msg.SenderEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
var messages = SplitMessage(FormattedMessage.RemoveMarkup(msg.Message));
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
EnqueueSpeechBubble(msg.SenderEntity, message, speechType);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData)
|
||||
{
|
||||
var bubble =
|
||||
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eye, _manager, _entities);
|
||||
|
||||
bubble.OnDied += SpeechBubbleDied;
|
||||
|
||||
if (_activeSpeechBubbles.TryGetValue(entity, out var existing))
|
||||
{
|
||||
// Push up existing bubbles above the mob's head.
|
||||
foreach (var existingBubble in existing)
|
||||
{
|
||||
existingBubble.VerticalOffset += bubble.ContentSize.Y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new List<SpeechBubble>();
|
||||
_activeSpeechBubbles.Add(entity, existing);
|
||||
}
|
||||
|
||||
existing.Add(bubble);
|
||||
_speechBubbleRoot.AddChild(bubble);
|
||||
|
||||
if (existing.Count > SpeechBubbleCap)
|
||||
{
|
||||
// Get the oldest to start fading fast.
|
||||
var last = existing[0];
|
||||
last.FadeNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void SpeechBubbleDied(EntityUid entity, SpeechBubble bubble)
|
||||
{
|
||||
RemoveSpeechBubble(entity, bubble);
|
||||
}
|
||||
|
||||
private void EnqueueSpeechBubble(EntityUid entity, string contents, SpeechBubble.SpeechType speechType)
|
||||
{
|
||||
// Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
|
||||
if (_entities.GetComponent<TransformComponent>(entity).MapID != _eye.CurrentMap)
|
||||
return;
|
||||
|
||||
if (!_queuedSpeechBubbles.TryGetValue(entity, out var queueData))
|
||||
{
|
||||
queueData = new SpeechBubbleQueueData();
|
||||
_queuedSpeechBubbles.Add(entity, queueData);
|
||||
}
|
||||
|
||||
queueData.MessageQueue.Enqueue(new SpeechBubbleData(contents, speechType));
|
||||
}
|
||||
|
||||
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
||||
{
|
||||
bubble.Dispose();
|
||||
|
||||
var list = _activeSpeechBubbles[entityUid];
|
||||
list.Remove(bubble);
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_activeSpeechBubbles.Remove(entityUid);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetChannelSelectorName(ChatSelectChannel channelSelector)
|
||||
{
|
||||
return channelSelector.ToString();
|
||||
}
|
||||
|
||||
public static char GetChannelSelectorPrefix(ChatSelectChannel channelSelector)
|
||||
{
|
||||
return channelSelector switch
|
||||
{
|
||||
ChatSelectChannel.Local => '.',
|
||||
ChatSelectChannel.Whisper => ',',
|
||||
ChatSelectChannel.Radio => ';',
|
||||
ChatSelectChannel.LOOC => '(',
|
||||
ChatSelectChannel.OOC => '[',
|
||||
ChatSelectChannel.Emotes => '@',
|
||||
ChatSelectChannel.Dead => '\\',
|
||||
ChatSelectChannel.Admin => ']',
|
||||
ChatSelectChannel.Console => '/',
|
||||
_ => ' '
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateChannelPermissions()
|
||||
{
|
||||
CanSendChannels = default;
|
||||
FilterableChannels = default;
|
||||
|
||||
// Can always send console stuff.
|
||||
CanSendChannels |= ChatSelectChannel.Console;
|
||||
|
||||
// can always send/recieve OOC
|
||||
CanSendChannels |= ChatSelectChannel.OOC;
|
||||
CanSendChannels |= ChatSelectChannel.LOOC;
|
||||
FilterableChannels |= ChatChannel.OOC;
|
||||
FilterableChannels |= ChatChannel.LOOC;
|
||||
|
||||
// can always hear server (nobody can actually send server messages).
|
||||
FilterableChannels |= ChatChannel.Server;
|
||||
|
||||
if (_state.CurrentState is GameplayStateBase)
|
||||
{
|
||||
// can always hear local / radio / emote when in the game
|
||||
FilterableChannels |= ChatChannel.Local;
|
||||
FilterableChannels |= ChatChannel.Whisper;
|
||||
FilterableChannels |= ChatChannel.Radio;
|
||||
FilterableChannels |= ChatChannel.Emotes;
|
||||
|
||||
// Can only send local / radio / emote when attached to a non-ghost entity.
|
||||
// TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
|
||||
if (_ghost is not {IsGhost: true})
|
||||
{
|
||||
CanSendChannels |= ChatSelectChannel.Local;
|
||||
CanSendChannels |= ChatSelectChannel.Whisper;
|
||||
CanSendChannels |= ChatSelectChannel.Radio;
|
||||
CanSendChannels |= ChatSelectChannel.Emotes;
|
||||
}
|
||||
}
|
||||
|
||||
// Only ghosts and admins can send / see deadchat.
|
||||
if (_admin.HasFlag(AdminFlags.Admin) || _ghost is {IsGhost: true})
|
||||
{
|
||||
FilterableChannels |= ChatChannel.Dead;
|
||||
CanSendChannels |= ChatSelectChannel.Dead;
|
||||
}
|
||||
|
||||
// only admins can see / filter asay
|
||||
if (_admin.HasFlag(AdminFlags.Admin))
|
||||
{
|
||||
FilterableChannels |= ChatChannel.Admin;
|
||||
CanSendChannels |= ChatSelectChannel.Admin;
|
||||
}
|
||||
|
||||
SelectableChannels = CanSendChannels & ~ChatSelectChannel.Console;
|
||||
|
||||
// Necessary so that we always have a channel to fall back to.
|
||||
DebugTools.Assert((CanSendChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
|
||||
DebugTools.Assert((FilterableChannels & ChatChannel.OOC) != 0, "OOC must always be available");
|
||||
DebugTools.Assert((SelectableChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
|
||||
|
||||
// let our chatbox know all the new settings
|
||||
CanSendChannelsChanged?.Invoke(CanSendChannels);
|
||||
FilterableChannelsChanged?.Invoke(FilterableChannels);
|
||||
SelectableChannelsChanged?.Invoke(SelectableChannels);
|
||||
}
|
||||
|
||||
public void ClearUnfilteredUnreads(ChatChannel channels)
|
||||
{
|
||||
foreach (var channel in _unreadMessages.Keys.ToArray())
|
||||
{
|
||||
if ((channels & channel) == 0)
|
||||
continue;
|
||||
|
||||
_unreadMessages[channel] = 0;
|
||||
UnreadMessageCountsUpdated?.Invoke(channel, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs delta)
|
||||
{
|
||||
UpdateQueuedSpeechBubbles(delta);
|
||||
}
|
||||
|
||||
private void UpdateQueuedSpeechBubbles(FrameEventArgs delta)
|
||||
{
|
||||
// Update queued speech bubbles.
|
||||
if (_queuedSpeechBubbles.Count == 0 || _examine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (entity, queueData) in _queuedSpeechBubbles.ShallowClone())
|
||||
{
|
||||
if (!_entities.EntityExists(entity))
|
||||
{
|
||||
_queuedSpeechBubbles.Remove(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
queueData.TimeLeft -= delta.DeltaSeconds;
|
||||
if (queueData.TimeLeft > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queueData.MessageQueue.Count == 0)
|
||||
{
|
||||
_queuedSpeechBubbles.Remove(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
var msg = queueData.MessageQueue.Dequeue();
|
||||
|
||||
queueData.TimeLeft += BubbleDelayBase + msg.Message.Length * BubbleDelayFactor;
|
||||
|
||||
// We keep the queue around while it has 0 items. This allows us to keep the timer.
|
||||
// When the timer hits 0 and there's no messages left, THEN we can clear it up.
|
||||
CreateSpeechBubble(entity, msg);
|
||||
}
|
||||
|
||||
var player = _player.LocalPlayer?.ControlledEntity;
|
||||
var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
|
||||
=> uid == data.compOwner || uid == data.attachedEntity;
|
||||
var playerPos = player != null
|
||||
? _entities.GetComponent<TransformComponent>(player.Value).MapPosition
|
||||
: MapCoordinates.Nullspace;
|
||||
|
||||
var occluded = player != null && _examine.IsOccluded(player.Value);
|
||||
|
||||
foreach (var (ent, bubs) in _activeSpeechBubbles)
|
||||
{
|
||||
if (_entities.Deleted(ent))
|
||||
{
|
||||
SetBubbles(bubs, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ent == player)
|
||||
{
|
||||
SetBubbles(bubs, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var otherPos = _entities.GetComponent<TransformComponent>(ent).MapPosition;
|
||||
|
||||
if (occluded && !ExamineSystemShared.InRangeUnOccluded(
|
||||
playerPos,
|
||||
otherPos, 0f,
|
||||
(ent, player), predicate))
|
||||
{
|
||||
SetBubbles(bubs, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
SetBubbles(bubs, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBubbles(List<SpeechBubble> bubbles, bool visible)
|
||||
{
|
||||
foreach (var bubble in bubbles)
|
||||
{
|
||||
bubble.Visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> SplitMessage(string msg)
|
||||
{
|
||||
// Split message into words separated by spaces.
|
||||
var words = msg.Split(' ');
|
||||
var messages = new List<string>();
|
||||
var currentBuffer = new List<string>();
|
||||
|
||||
// Really shoddy way to approximate word length.
|
||||
// Yes, I am aware of all the crimes here.
|
||||
// TODO: Improve this to use actual glyph width etc..
|
||||
var currentWordLength = 0;
|
||||
foreach (var word in words)
|
||||
{
|
||||
// +1 for the space.
|
||||
currentWordLength += word.Length + 1;
|
||||
|
||||
if (currentWordLength > SingleBubbleCharLimit)
|
||||
{
|
||||
// Too long for the current speech bubble, flush it.
|
||||
messages.Add(string.Join(" ", currentBuffer));
|
||||
currentBuffer.Clear();
|
||||
|
||||
currentWordLength = word.Length;
|
||||
|
||||
if (currentWordLength > SingleBubbleCharLimit)
|
||||
{
|
||||
// Word is STILL too long.
|
||||
// Truncate it with an ellipse.
|
||||
messages.Add($"{word.Substring(0, SingleBubbleCharLimit - 3)}...");
|
||||
currentWordLength = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentBuffer.Add(word);
|
||||
}
|
||||
|
||||
if (currentBuffer.Count != 0)
|
||||
{
|
||||
// Don't forget the last bubble.
|
||||
messages.Add(string.Join(" ", currentBuffer));
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel)
|
||||
{
|
||||
if (channel == ChatSelectChannel.Local && _ghost is {IsGhost: true})
|
||||
return ChatSelectChannel.Dead;
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
public (ChatSelectChannel channel, ReadOnlyMemory<char> text) SplitInputContents(string inputText)
|
||||
{
|
||||
var text = inputText.AsMemory().Trim();
|
||||
if (text.Length == 0)
|
||||
return default;
|
||||
|
||||
var prefixChar = text.Span[0];
|
||||
var channel = PrefixToChannel.GetValueOrDefault(prefixChar);
|
||||
|
||||
if ((CanSendChannels & channel) != 0)
|
||||
// Cut off prefix if it's valid and we can use the channel in question.
|
||||
text = text[1..];
|
||||
else
|
||||
channel = 0;
|
||||
|
||||
channel = MapLocalIfGhost(channel);
|
||||
|
||||
// Trim from start again to cut out any whitespace between the prefix and message, if any.
|
||||
return (channel, text.TrimStart());
|
||||
}
|
||||
|
||||
public void SendMessage(ChatBox box, ChatSelectChannel channel)
|
||||
{
|
||||
_typingIndicator?.ClientSubmittedChatText();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(box.ChatInput.Input.Text))
|
||||
{
|
||||
var (prefixChannel, text) = SplitInputContents(box.ChatInput.Input.Text);
|
||||
|
||||
// Check if message is longer than the character limit
|
||||
if (text.Length > MaxMessageLength)
|
||||
{
|
||||
var locWarning = Loc.GetString("chat-manager-max-message-length",
|
||||
("maxMessageLength", MaxMessageLength));
|
||||
box.AddLine(locWarning, Color.Orange);
|
||||
return;
|
||||
}
|
||||
|
||||
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
||||
}
|
||||
|
||||
box.ChatInput.Input.Clear();
|
||||
box.UpdateSelectedChannel();
|
||||
box.ChatInput.ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
private void OnChatMessage(MsgChatMessage msg)
|
||||
{
|
||||
// Log all incoming chat to repopulate when filter is un-toggled
|
||||
if (!msg.HideChat)
|
||||
{
|
||||
var storedMessage = new StoredChatMessage(msg);
|
||||
History.Add(storedMessage);
|
||||
MessageAdded?.Invoke(storedMessage);
|
||||
|
||||
if (!storedMessage.Read)
|
||||
{
|
||||
_sawmill.Debug($"Message filtered: {storedMessage.Channel}: {storedMessage.Message}");
|
||||
if (!_unreadMessages.TryGetValue(msg.Channel, out var count))
|
||||
count = 0;
|
||||
|
||||
count += 1;
|
||||
_unreadMessages[msg.Channel] = count;
|
||||
UnreadMessageCountsUpdated?.Invoke(msg.Channel, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Local messages that have an entity attached get a speech bubble.
|
||||
if (msg.SenderEntity == default)
|
||||
return;
|
||||
|
||||
switch (msg.Channel)
|
||||
{
|
||||
case ChatChannel.Local:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
||||
break;
|
||||
|
||||
case ChatChannel.Whisper:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Whisper);
|
||||
break;
|
||||
|
||||
case ChatChannel.Dead:
|
||||
if (_ghost is not {IsGhost: true})
|
||||
break;
|
||||
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
||||
break;
|
||||
|
||||
case ChatChannel.Emotes:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public char GetPrefixFromChannel(ChatSelectChannel channel)
|
||||
{
|
||||
return ChannelPrefixes.GetValueOrDefault(channel);
|
||||
}
|
||||
|
||||
public void RegisterChat(ChatBox chat)
|
||||
{
|
||||
_chats.Add(chat);
|
||||
}
|
||||
|
||||
public void UnregisterChat(ChatBox chat)
|
||||
{
|
||||
_chats.Remove(chat);
|
||||
}
|
||||
|
||||
public ChatSelectChannel GetPreferredChannel()
|
||||
{
|
||||
return MapLocalIfGhost(PreferredChannel);
|
||||
}
|
||||
|
||||
private readonly record struct SpeechBubbleData(string Message, SpeechBubble.SpeechType Type);
|
||||
|
||||
private sealed class SpeechBubbleQueueData
|
||||
{
|
||||
/// <summary>
|
||||
/// Time left until the next speech bubble can appear.
|
||||
/// </summary>
|
||||
public float TimeLeft { get; set; }
|
||||
|
||||
public Queue<SpeechBubbleData> MessageQueue { get; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
public sealed class ChannelFilterButton : ContainerButton
|
||||
{
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
private readonly TextureRect _textureRect;
|
||||
public readonly ChannelFilterPopup ChatFilterPopup;
|
||||
private readonly ChatUIController _chatUIController;
|
||||
private const int FilterDropdownOffset = 120;
|
||||
|
||||
public ChannelFilterButton()
|
||||
{
|
||||
_chatUIController = UserInterfaceManager.GetUIController<ChatUIController>();
|
||||
var filterTexture = IoCManager.Resolve<IResourceCache>()
|
||||
.GetTexture("/Textures/Interface/Nano/filter.svg.96dpi.png");
|
||||
|
||||
// needed for same reason as ChannelSelectorButton
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
|
||||
AddChild(
|
||||
(_textureRect = new TextureRect
|
||||
{
|
||||
Texture = filterTexture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center
|
||||
})
|
||||
);
|
||||
ToggleMode = true;
|
||||
OnToggled += OnFilterButtonToggled;
|
||||
ChatFilterPopup = UserInterfaceManager.CreatePopup<ChannelFilterPopup>();
|
||||
ChatFilterPopup.OnVisibilityChanged += PopupVisibilityChanged;
|
||||
|
||||
_chatUIController.FilterableChannelsChanged += ChatFilterPopup.SetChannels;
|
||||
_chatUIController.UnreadMessageCountsUpdated += ChatFilterPopup.UpdateUnread;
|
||||
ChatFilterPopup.SetChannels(_chatUIController.FilterableChannels);
|
||||
}
|
||||
|
||||
private void PopupVisibilityChanged(Control control)
|
||||
{
|
||||
Pressed = control.Visible;
|
||||
}
|
||||
|
||||
private void OnFilterButtonToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalPos = GlobalPosition;
|
||||
var (minX, minY) = ChatFilterPopup.MinSize;
|
||||
var box = UIBox2.FromDimensions(globalPos - (FilterDropdownOffset, 0),
|
||||
(Math.Max(minX, ChatFilterPopup.MinWidth), minY));
|
||||
ChatFilterPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
ChatFilterPopup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use) return;
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_textureRect == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_textureRect.ModulateSelfOverride = ColorNormal;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_textureRect.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_textureRect.ModulateSelfOverride = ColorHovered;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_chatUIController.FilterableChannelsChanged -= ChatFilterPopup.SetChannels;
|
||||
_chatUIController.UnreadMessageCountsUpdated -= ChatFilterPopup.UpdateUnread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
public sealed class ChannelFilterCheckbox : CheckBox
|
||||
{
|
||||
public readonly ChatChannel Channel;
|
||||
|
||||
public bool IsHidden => Parent == null;
|
||||
|
||||
public ChannelFilterCheckbox(ChatChannel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
Text = Loc.GetString($"hud-chatbox-channel-{Channel}");
|
||||
}
|
||||
|
||||
private void UpdateText(int? unread)
|
||||
{
|
||||
var name = Loc.GetString($"hud-chatbox-channel-{Channel}");
|
||||
|
||||
if (unread > 0)
|
||||
// todo: proper fluent stuff here.
|
||||
name += " (" + (unread > 9 ? "9+" : unread) + ")";
|
||||
|
||||
Text = name;
|
||||
}
|
||||
|
||||
public void UpdateUnreadCount(int? unread)
|
||||
{
|
||||
UpdateText(unread);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<controls:ChannelFilterPopup
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls">
|
||||
<PanelContainer Name="FilterPopupPanel" StyleClasses="BorderedWindowPanel">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control MinSize="4 0"/>
|
||||
<BoxContainer Name="FilterVBox" MinWidth="110" Margin="0 10" Orientation="Vertical" SeparationOverride="4"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:ChannelFilterPopup>
|
||||
@@ -0,0 +1,94 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelFilterPopup : Popup
|
||||
{
|
||||
// order in which the available channel filters show up when available
|
||||
private static readonly ChatChannel[] ChannelFilterOrder =
|
||||
{
|
||||
ChatChannel.Local,
|
||||
ChatChannel.Whisper,
|
||||
ChatChannel.Emotes,
|
||||
ChatChannel.Radio,
|
||||
ChatChannel.LOOC,
|
||||
ChatChannel.OOC,
|
||||
ChatChannel.Dead,
|
||||
ChatChannel.Admin,
|
||||
ChatChannel.Server
|
||||
};
|
||||
|
||||
private readonly Dictionary<ChatChannel, ChannelFilterCheckbox> _filterStates = new();
|
||||
|
||||
public event Action<ChatChannel, bool>? OnChannelFilter;
|
||||
|
||||
public ChannelFilterPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public bool IsActive(ChatChannel channel)
|
||||
{
|
||||
return _filterStates.TryGetValue(channel, out var checkbox) && checkbox.Pressed;
|
||||
}
|
||||
|
||||
public ChatChannel GetActive()
|
||||
{
|
||||
ChatChannel active = 0;
|
||||
|
||||
foreach (var (key, value) in _filterStates)
|
||||
{
|
||||
if (value.IsHidden || !value.Pressed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
active |= key;
|
||||
}
|
||||
|
||||
return active;
|
||||
}
|
||||
|
||||
public void SetChannels(ChatChannel channels)
|
||||
{
|
||||
foreach (var channel in ChannelFilterOrder)
|
||||
{
|
||||
if (!_filterStates.TryGetValue(channel, out var checkbox))
|
||||
{
|
||||
checkbox = new ChannelFilterCheckbox(channel);
|
||||
_filterStates.Add(channel, checkbox);
|
||||
checkbox.OnPressed += CheckboxPressed;
|
||||
checkbox.Pressed = true;
|
||||
}
|
||||
|
||||
if ((channels & channel) == 0)
|
||||
{
|
||||
if (checkbox.Parent == FilterVBox)
|
||||
{
|
||||
FilterVBox.RemoveChild(checkbox);
|
||||
}
|
||||
}
|
||||
else if (checkbox.IsHidden)
|
||||
{
|
||||
FilterVBox.AddChild(checkbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckboxPressed(ButtonEventArgs args)
|
||||
{
|
||||
var checkbox = (ChannelFilterCheckbox) args.Button;
|
||||
OnChannelFilter?.Invoke(checkbox.Channel, checkbox.Pressed);
|
||||
}
|
||||
|
||||
public void UpdateUnread(ChatChannel channel, int? unread)
|
||||
{
|
||||
if (_filterStates.TryGetValue(channel, out var checkbox))
|
||||
checkbox.UpdateUnreadCount(unread);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Only needed to avoid the issue where right click on the button closes the popup
|
||||
/// but leaves the button highlighted.
|
||||
/// </summary>
|
||||
public sealed class ChannelSelectorButton : Button
|
||||
{
|
||||
private readonly ChannelSelectorPopup _channelSelectorPopup;
|
||||
public event Action<ChatSelectChannel>? OnChannelSelect;
|
||||
|
||||
public ChatSelectChannel SelectedChannel { get; private set; }
|
||||
|
||||
private const int SelectorDropdownOffset = 38;
|
||||
|
||||
public ChannelSelectorButton()
|
||||
{
|
||||
// needed so the popup is untoggled regardless of which key is pressed when hovering this button.
|
||||
// If we don't have this, then right clicking the button while it's toggled on will hide
|
||||
// the popup but keep the button toggled on
|
||||
Name = "ChannelSelector";
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
ToggleMode = true;
|
||||
OnToggled += OnSelectorButtonToggled;
|
||||
_channelSelectorPopup = UserInterfaceManager.CreatePopup<ChannelSelectorPopup>();
|
||||
_channelSelectorPopup.Selected += OnChannelSelected;
|
||||
_channelSelectorPopup.OnVisibilityChanged += OnPopupVisibilityChanged;
|
||||
|
||||
if (_channelSelectorPopup.FirstChannel is { } firstSelector)
|
||||
{
|
||||
Select(firstSelector);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChannelSelected(ChatSelectChannel channel)
|
||||
{
|
||||
Select(channel);
|
||||
}
|
||||
|
||||
private void OnPopupVisibilityChanged(Control control)
|
||||
{
|
||||
Pressed = control.Visible;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use) return;
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
public void Select(ChatSelectChannel channel)
|
||||
{
|
||||
if (_channelSelectorPopup.Visible)
|
||||
{
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
|
||||
if (SelectedChannel == channel) return;
|
||||
SelectedChannel = channel;
|
||||
UpdateChannelSelectButton(channel);
|
||||
|
||||
OnChannelSelect?.Invoke(channel);
|
||||
}
|
||||
|
||||
public string ChannelSelectorName(ChatSelectChannel channel)
|
||||
{
|
||||
return Loc.GetString($"hud-chatbox-select-channel-{channel}");
|
||||
}
|
||||
|
||||
public Color ChannelSelectColor(ChatSelectChannel channel)
|
||||
{
|
||||
return channel switch
|
||||
{
|
||||
ChatSelectChannel.Radio => Color.LimeGreen,
|
||||
ChatSelectChannel.LOOC => Color.MediumTurquoise,
|
||||
ChatSelectChannel.OOC => Color.LightSkyBlue,
|
||||
ChatSelectChannel.Dead => Color.MediumPurple,
|
||||
ChatSelectChannel.Admin => Color.Red,
|
||||
_ => Color.DarkGray
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateChannelSelectButton(ChatSelectChannel channel)
|
||||
{
|
||||
Text = ChannelSelectorName(channel);
|
||||
Modulate = ChannelSelectColor(channel);
|
||||
}
|
||||
|
||||
private void OnSelectorButtonToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalLeft = GlobalPosition.X;
|
||||
var globalBot = GlobalPosition.Y + Height;
|
||||
var box = UIBox2.FromDimensions((globalLeft, globalBot), (SizeBox.Width, SelectorDropdownOffset));
|
||||
_channelSelectorPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
public sealed class ChannelSelectorItemButton : Button
|
||||
{
|
||||
public readonly ChatSelectChannel Channel;
|
||||
|
||||
public bool IsHidden => Parent == null;
|
||||
|
||||
public ChannelSelectorItemButton(ChatSelectChannel selector)
|
||||
{
|
||||
Channel = selector;
|
||||
AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton);
|
||||
Text = ChatUIController.GetChannelSelectorName(selector);
|
||||
var prefix = ChatUIController.GetChannelSelectorPrefix(selector);
|
||||
if (prefix != default) Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
public sealed class ChannelSelectorPopup : Popup
|
||||
{
|
||||
// order in which the channels show up in the channel selector
|
||||
public static readonly ChatSelectChannel[] ChannelSelectorOrder =
|
||||
{
|
||||
ChatSelectChannel.Local,
|
||||
ChatSelectChannel.Whisper,
|
||||
ChatSelectChannel.Emotes,
|
||||
ChatSelectChannel.Radio,
|
||||
ChatSelectChannel.LOOC,
|
||||
ChatSelectChannel.OOC,
|
||||
ChatSelectChannel.Dead,
|
||||
ChatSelectChannel.Admin
|
||||
// NOTE: Console is not in there and it can never be permanently selected.
|
||||
// You can, however, still submit commands as console by prefixing with /.
|
||||
};
|
||||
|
||||
private readonly BoxContainer _channelSelectorHBox;
|
||||
private readonly Dictionary<ChatSelectChannel, ChannelSelectorItemButton> _selectorStates = new();
|
||||
private readonly ChatUIController _chatUIController;
|
||||
|
||||
public event Action<ChatSelectChannel>? Selected;
|
||||
|
||||
public ChannelSelectorPopup()
|
||||
{
|
||||
_channelSelectorHBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 1
|
||||
};
|
||||
|
||||
_chatUIController = UserInterfaceManager.GetUIController<ChatUIController>();
|
||||
_chatUIController.SelectableChannelsChanged += SetChannels;
|
||||
SetChannels(_chatUIController.SelectableChannels);
|
||||
|
||||
AddChild(_channelSelectorHBox);
|
||||
}
|
||||
|
||||
public ChatSelectChannel? FirstChannel
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var selector in _selectorStates.Values)
|
||||
{
|
||||
if (!selector.IsHidden)
|
||||
return selector.Channel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*public ChatSelectChannel NextChannel()
|
||||
{
|
||||
var nextChannel = ChatUIController.GetNextChannelSelector(_activeSelector);
|
||||
var index = 0;
|
||||
while (_selectorStates[(int)nextChannel].IsHidden && index <= _selectorStates.Count)
|
||||
{
|
||||
nextChannel = ChatUIController.GetNextChannelSelector(nextChannel);
|
||||
index++;
|
||||
}
|
||||
_activeSelector = nextChannel;
|
||||
return nextChannel;
|
||||
}
|
||||
|
||||
|
||||
private void SetupChannels(ChatUIController.ChannelSelectorSetup[] selectorData)
|
||||
{
|
||||
_channelSelectorHBox.DisposeAllChildren(); //cleanup old toggles
|
||||
_selectorStates.Clear();
|
||||
foreach (var channelSelectorData in selectorData)
|
||||
{
|
||||
var newSelectorButton = new ChannelSelectorItemButton(channelSelectorData);
|
||||
_selectorStates.Add(newSelectorButton);
|
||||
if (!newSelectorButton.IsHidden)
|
||||
{
|
||||
_channelSelectorHBox.AddChild(newSelectorButton);
|
||||
}
|
||||
newSelectorButton.OnPressed += OnSelectorPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectorPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (_selectorButton == null) return;
|
||||
_selectorButton.SelectedChannel = ((ChannelSelectorItemButton) args.Button).Channel;
|
||||
}
|
||||
|
||||
public void HideChannels(params ChatChannel[] channels)
|
||||
{
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
if (!ChatUIController.ChannelToSelector.TryGetValue(channel, out var selector)) continue;
|
||||
var selectorbutton = _selectorStates[(int)selector];
|
||||
if (!selectorbutton.IsHidden)
|
||||
{
|
||||
_channelSelectorHBox.RemoveChild(selectorbutton);
|
||||
if (_activeSelector != selector) continue; // do nothing
|
||||
if (_channelSelectorHBox.Children.First() is ChannelSelectorItemButton button)
|
||||
{
|
||||
_activeSelector = button.Channel;
|
||||
}
|
||||
else
|
||||
{
|
||||
_activeSelector = ChatSelectChannel.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private bool IsPreferredAvailable()
|
||||
{
|
||||
var preferred = _chatUIController.MapLocalIfGhost(_chatUIController.GetPreferredChannel());
|
||||
return _selectorStates.TryGetValue(preferred, out var selector) &&
|
||||
!selector.IsHidden;
|
||||
}
|
||||
|
||||
public void SetChannels(ChatSelectChannel channels)
|
||||
{
|
||||
var wasPreferredAvailable = IsPreferredAvailable();
|
||||
|
||||
_channelSelectorHBox.RemoveAllChildren();
|
||||
|
||||
foreach (var channel in ChannelSelectorOrder)
|
||||
{
|
||||
if (!_selectorStates.TryGetValue(channel, out var selector))
|
||||
{
|
||||
selector = new ChannelSelectorItemButton(channel);
|
||||
_selectorStates.Add(channel, selector);
|
||||
selector.OnPressed += OnSelectorPressed;
|
||||
}
|
||||
|
||||
if ((channels & channel) == 0)
|
||||
{
|
||||
if (selector.Parent == _channelSelectorHBox)
|
||||
{
|
||||
_channelSelectorHBox.RemoveChild(selector);
|
||||
}
|
||||
}
|
||||
else if (selector.IsHidden)
|
||||
{
|
||||
_channelSelectorHBox.AddChild(selector);
|
||||
}
|
||||
}
|
||||
|
||||
var isPreferredAvailable = IsPreferredAvailable();
|
||||
if (!wasPreferredAvailable && isPreferredAvailable)
|
||||
{
|
||||
Select(_chatUIController.GetPreferredChannel());
|
||||
}
|
||||
else if (wasPreferredAvailable && !isPreferredAvailable)
|
||||
{
|
||||
Select(ChatSelectChannel.OOC);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectorPressed(ButtonEventArgs args)
|
||||
{
|
||||
var button = (ChannelSelectorItemButton) args.Button;
|
||||
Select(button.Channel);
|
||||
}
|
||||
|
||||
private void Select(ChatSelectChannel channel)
|
||||
{
|
||||
Selected?.Invoke(channel);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_chatUIController.SelectableChannelsChanged -= SetChannels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class ChatInputBox : PanelContainer
|
||||
{
|
||||
public readonly ChannelSelectorButton ChannelSelector;
|
||||
public readonly HistoryLineEdit Input;
|
||||
public readonly ChannelFilterButton FilterButton;
|
||||
protected readonly BoxContainer Container;
|
||||
protected ChatChannel ActiveChannel { get; private set; } = ChatChannel.Local;
|
||||
|
||||
public ChatInputBox()
|
||||
{
|
||||
Container = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 4
|
||||
};
|
||||
AddChild(Container);
|
||||
|
||||
ChannelSelector = new ChannelSelectorButton
|
||||
{
|
||||
Name = "ChannelSelector",
|
||||
ToggleMode = true,
|
||||
StyleClasses = {"chatSelectorOptionButton"},
|
||||
MinWidth = 75
|
||||
};
|
||||
Container.AddChild(ChannelSelector);
|
||||
Input = new HistoryLineEdit
|
||||
{
|
||||
Name = "Input",
|
||||
PlaceHolder = Loc.GetString("hud-chatbox-info"),
|
||||
HorizontalExpand = true,
|
||||
StyleClasses = {"chatLineEdit"}
|
||||
};
|
||||
Container.AddChild(Input);
|
||||
FilterButton = new ChannelFilterButton
|
||||
{
|
||||
Name = "FilterButton",
|
||||
StyleClasses = {"chatFilterOptionButton"}
|
||||
};
|
||||
Container.AddChild(FilterButton);
|
||||
ChannelSelector.OnChannelSelect += UpdateActiveChannel;
|
||||
}
|
||||
|
||||
private void UpdateActiveChannel(ChatSelectChannel selectedChannel)
|
||||
{
|
||||
ActiveChannel = (ChatChannel) selectedChannel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<widgets:ChatBox
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls"
|
||||
MouseFilter="Stop"
|
||||
MinSize="465 225">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#25252AAA" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="4">
|
||||
<OutputPanel Name="Contents" VerticalExpand="True" />
|
||||
<controls:ChatInputBox Name="ChatInput" Access="Public" Margin="2"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</widgets:ChatBox>
|
||||
@@ -0,0 +1,214 @@
|
||||
using Content.Client.Chat;
|
||||
using Content.Client.Chat.TypingIndicator;
|
||||
using Content.Client.UserInterface.Systems.Chat.Controls;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.LineEdit;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
#pragma warning disable RA0003
|
||||
public partial class ChatBox : Control
|
||||
#pragma warning restore RA0003
|
||||
{
|
||||
private readonly ChatUIController _controller;
|
||||
|
||||
public bool Main { get; set; }
|
||||
|
||||
public ChatSelectChannel SelectedChannel => ChatInput.ChannelSelector.SelectedChannel;
|
||||
|
||||
public ChatBox()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ChatInput.Input.OnTextEntered += OnTextEntered;
|
||||
ChatInput.Input.OnKeyBindDown += OnKeyBindDown;
|
||||
ChatInput.Input.OnTextChanged += OnTextChanged;
|
||||
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
|
||||
ChatInput.FilterButton.ChatFilterPopup.OnChannelFilter += OnChannelFilter;
|
||||
|
||||
_controller = UserInterfaceManager.GetUIController<ChatUIController>();
|
||||
_controller.MessageAdded += OnMessageAdded;
|
||||
_controller.RegisterChat(this);
|
||||
}
|
||||
|
||||
private void OnTextEntered(LineEditEventArgs args)
|
||||
{
|
||||
_controller.SendMessage(this, SelectedChannel);
|
||||
}
|
||||
|
||||
private void OnMessageAdded(StoredChatMessage msg)
|
||||
{
|
||||
var text = FormattedMessage.EscapeText(msg.Message);
|
||||
if (!string.IsNullOrEmpty(msg.MessageWrap))
|
||||
{
|
||||
text = string.Format(msg.MessageWrap, text);
|
||||
}
|
||||
|
||||
Logger.DebugS("chat", $"{msg.Channel}: {text}");
|
||||
if (!ChatInput.FilterButton.ChatFilterPopup.IsActive(msg.Channel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
msg.Read = true;
|
||||
|
||||
var color = msg.MessageColorOverride != Color.Transparent
|
||||
? msg.MessageColorOverride
|
||||
: msg.Channel.TextColor();
|
||||
|
||||
AddLine(text, color);
|
||||
}
|
||||
|
||||
private void OnChannelSelect(ChatSelectChannel channel)
|
||||
{
|
||||
UpdateSelectedChannel();
|
||||
}
|
||||
|
||||
private void OnChannelFilter(ChatChannel channel, bool active)
|
||||
{
|
||||
Contents.Clear();
|
||||
|
||||
foreach (var message in _controller.History)
|
||||
{
|
||||
OnMessageAdded(message);
|
||||
}
|
||||
|
||||
if (active)
|
||||
{
|
||||
_controller.ClearUnfilteredUnreads(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddLine(string message, Color color)
|
||||
{
|
||||
var formatted = new FormattedMessage(3);
|
||||
formatted.PushColor(color);
|
||||
formatted.AddMarkup(message);
|
||||
formatted.Pop();
|
||||
Contents.AddMessage(formatted);
|
||||
}
|
||||
|
||||
public void UpdateSelectedChannel()
|
||||
{
|
||||
var (prefixChannel, _) = _controller.SplitInputContents(ChatInput.Input.Text);
|
||||
var channel = prefixChannel == 0 ? SelectedChannel : prefixChannel;
|
||||
|
||||
ChatInput.ChannelSelector.UpdateChannelSelectButton(channel);
|
||||
}
|
||||
|
||||
public void Focus(ChatSelectChannel? channel = null)
|
||||
{
|
||||
var input = ChatInput.Input;
|
||||
var selectStart = Index.End;
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
channel = _controller.MapLocalIfGhost(channel.Value);
|
||||
|
||||
// Channel not selectable, just do NOTHING (not even focus).
|
||||
if ((_controller.SelectableChannels & channel.Value) == 0)
|
||||
return;
|
||||
|
||||
var (_, text) = _controller.SplitInputContents(input.Text);
|
||||
|
||||
var newPrefix = _controller.GetPrefixFromChannel(channel.Value);
|
||||
DebugTools.Assert(newPrefix != default, "Focus channel must have prefix!");
|
||||
|
||||
if (channel == SelectedChannel)
|
||||
{
|
||||
// New selected channel is just the selected channel,
|
||||
// just remove prefix (if any) and leave text unchanged.
|
||||
|
||||
input.Text = text.ToString();
|
||||
selectStart = Index.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Change prefix to new focused channel prefix and leave text unchanged.
|
||||
input.Text = string.Concat(newPrefix.ToString(), " ", text.Span);
|
||||
selectStart = Index.FromStart(2);
|
||||
}
|
||||
|
||||
ChatInput.ChannelSelector.Select(channel.Value);
|
||||
}
|
||||
|
||||
input.IgnoreNext = true;
|
||||
input.GrabKeyboardFocus();
|
||||
|
||||
input.CursorPosition = input.Text.Length;
|
||||
input.SelectionStart = selectStart.GetOffset(input.Text.Length);
|
||||
}
|
||||
|
||||
public void CycleChatChannel(bool forward)
|
||||
{
|
||||
var idx = Array.IndexOf(ChannelSelectorPopup.ChannelSelectorOrder, SelectedChannel);
|
||||
do
|
||||
{
|
||||
// go over every channel until we find one we can actually select.
|
||||
idx += forward ? 1 : -1;
|
||||
idx = MathHelper.Mod(idx, ChannelSelectorPopup.ChannelSelectorOrder.Length);
|
||||
} while ((_controller.SelectableChannels & ChannelSelectorPopup.ChannelSelectorOrder[idx]) == 0);
|
||||
|
||||
SafelySelectChannel(ChannelSelectorPopup.ChannelSelectorOrder[idx]);
|
||||
}
|
||||
|
||||
public void SafelySelectChannel(ChatSelectChannel toSelect)
|
||||
{
|
||||
toSelect = _controller.MapLocalIfGhost(toSelect);
|
||||
if ((_controller.SelectableChannels & toSelect) == 0)
|
||||
return;
|
||||
|
||||
ChatInput.ChannelSelector.Select(toSelect);
|
||||
}
|
||||
|
||||
private void OnKeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.TextReleaseFocus)
|
||||
{
|
||||
ChatInput.Input.ReleaseKeyboardFocus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelForward)
|
||||
{
|
||||
CycleChatChannel(true);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelBackward)
|
||||
{
|
||||
CycleChatChannel(false);
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTextChanged(LineEditEventArgs args)
|
||||
{
|
||||
// Update channel select button to correct channel if we have a prefix.
|
||||
UpdateSelectedChannel();
|
||||
|
||||
// Warn typing indicator about change
|
||||
EntitySystem.Get<TypingIndicatorSystem>().ClientChangedChatText();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing) return;
|
||||
_controller.UnregisterChat(this);
|
||||
ChatInput.Input.OnTextEntered -= OnTextEntered;
|
||||
ChatInput.Input.OnKeyBindDown -= OnKeyBindDown;
|
||||
ChatInput.Input.OnTextChanged -= OnTextChanged;
|
||||
ChatInput.ChannelSelector.OnChannelSelect -= OnChannelSelect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
|
||||
public sealed class ResizableChatBox : ChatBox
|
||||
{
|
||||
public ResizableChatBox()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
// TODO: Revisit the resizing stuff after https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
|
||||
// Probably not "supposed" to inject IClyde, but I give up.
|
||||
// I can't find any other way to allow this control to properly resize when the
|
||||
// window is resized. Resized() isn't reliably called when resizing the window,
|
||||
// and layoutcontainer anchor / margin don't seem to adjust how we need
|
||||
// them to when the window is resized. We need it to be able to resize
|
||||
// within some bounds so that it doesn't overlap other UI elements, while still
|
||||
// being freely resizable within those bounds.
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
private const int DragMarginSize = 7;
|
||||
private const int MinDistanceFromBottom = 255;
|
||||
private const int MinLeft = 500;
|
||||
private DragMode _currentDrag = DragMode.None;
|
||||
private Vector2 _dragOffsetTopLeft;
|
||||
private Vector2 _dragOffsetBottomRight;
|
||||
|
||||
private byte _clampIn;
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_clyde.OnWindowResized += ClydeOnOnWindowResized;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_currentDrag = GetDragModeFor(args.RelativePosition);
|
||||
|
||||
if (_currentDrag != DragMode.None)
|
||||
{
|
||||
_dragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position;
|
||||
_dragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale;
|
||||
}
|
||||
}
|
||||
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
if (_currentDrag != DragMode.None)
|
||||
{
|
||||
_dragOffsetTopLeft = _dragOffsetBottomRight = Vector2.Zero;
|
||||
_currentDrag = DragMode.None;
|
||||
|
||||
// If this is done in MouseDown, Godot won't fire MouseUp as you need focus to receive MouseUps.
|
||||
UserInterfaceManager.KeyboardFocused?.ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
base.KeyBindUp(args);
|
||||
}
|
||||
|
||||
|
||||
// TODO: this drag and drop stuff is somewhat duplicated from Robust BaseWindow but also modified
|
||||
[Flags]
|
||||
private enum DragMode : byte
|
||||
{
|
||||
None = 0,
|
||||
Bottom = 1 << 1,
|
||||
Left = 1 << 2
|
||||
}
|
||||
|
||||
private DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
var mode = DragMode.None;
|
||||
|
||||
if (relativeMousePos.Y > Size.Y - DragMarginSize)
|
||||
{
|
||||
mode = DragMode.Bottom;
|
||||
}
|
||||
|
||||
if (relativeMousePos.X < DragMarginSize)
|
||||
{
|
||||
mode |= DragMode.Left;
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
protected override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
|
||||
if (Parent == null)
|
||||
return;
|
||||
|
||||
if (_currentDrag == DragMode.None)
|
||||
{
|
||||
var cursor = CursorShape.Arrow;
|
||||
var previewDragMode = GetDragModeFor(args.RelativePosition);
|
||||
switch (previewDragMode)
|
||||
{
|
||||
case DragMode.Bottom:
|
||||
cursor = CursorShape.VResize;
|
||||
break;
|
||||
|
||||
case DragMode.Left:
|
||||
cursor = CursorShape.HResize;
|
||||
break;
|
||||
|
||||
case DragMode.Bottom | DragMode.Left:
|
||||
cursor = CursorShape.Crosshair;
|
||||
break;
|
||||
}
|
||||
|
||||
DefaultCursorShape = cursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
var top = Rect.Top;
|
||||
var bottom = Rect.Bottom;
|
||||
var left = Rect.Left;
|
||||
var right = Rect.Right;
|
||||
var (minSizeX, minSizeY) = MinSize;
|
||||
if ((_currentDrag & DragMode.Bottom) == DragMode.Bottom)
|
||||
{
|
||||
bottom = Math.Max(args.GlobalPosition.Y + _dragOffsetBottomRight.Y, top + minSizeY);
|
||||
}
|
||||
|
||||
if ((_currentDrag & DragMode.Left) == DragMode.Left)
|
||||
{
|
||||
var maxX = right - minSizeX;
|
||||
left = Math.Min(args.GlobalPosition.X - _dragOffsetTopLeft.X, maxX);
|
||||
}
|
||||
|
||||
ClampSize(left, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
base.UIScaleChanged();
|
||||
ClampAfterDelay();
|
||||
}
|
||||
|
||||
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
|
||||
{
|
||||
ClampAfterDelay();
|
||||
}
|
||||
|
||||
private void ClampAfterDelay()
|
||||
{
|
||||
_clampIn = 2;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
// we do the clamping after a delay (after UI scale / window resize)
|
||||
// because we need to wait for our parent container to properly resize
|
||||
// first, so we can calculate where we should go. If we do it right away,
|
||||
// we won't have the correct values from the parent to know how to adjust our margins.
|
||||
if (_clampIn <= 0)
|
||||
return;
|
||||
|
||||
_clampIn -= 1;
|
||||
if (_clampIn == 0)
|
||||
ClampSize();
|
||||
}
|
||||
|
||||
private void ClampSize(float? desiredLeft = null, float? desiredBottom = null)
|
||||
{
|
||||
if (Parent == null)
|
||||
return;
|
||||
|
||||
// var top = Rect.Top;
|
||||
var right = Rect.Right;
|
||||
var left = desiredLeft ?? Rect.Left;
|
||||
var bottom = desiredBottom ?? Rect.Bottom;
|
||||
|
||||
// clamp so it doesn't go too high or low (leave space for alerts UI)
|
||||
var maxBottom = Parent.Size.Y - MinDistanceFromBottom;
|
||||
if (maxBottom <= MinHeight)
|
||||
{
|
||||
// we can't fit in our given space (window made awkwardly small), so give up
|
||||
// and overlap at our min height
|
||||
bottom = MinHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
bottom = Math.Clamp(bottom, MinHeight, maxBottom);
|
||||
}
|
||||
|
||||
var maxLeft = Parent.Size.X - MinWidth;
|
||||
if (maxLeft <= MinLeft)
|
||||
{
|
||||
// window too narrow, give up and overlap at our max left
|
||||
left = maxLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
left = Math.Clamp(left, MinLeft, maxLeft);
|
||||
}
|
||||
|
||||
LayoutContainer.SetMarginLeft(this, -((right + 10) - left));
|
||||
LayoutContainer.SetMarginBottom(this, bottom);
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
if (_currentDrag == DragMode.None)
|
||||
DefaultCursorShape = CursorShape.Arrow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Client.Construction.UI;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Crafting;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CraftingUIController : UIController, IOnStateChanged<GameplayState>
|
||||
{
|
||||
private ConstructionMenuPresenter? _presenter;
|
||||
private MenuButton? _craftingButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_presenter == null);
|
||||
_presenter = new ConstructionMenuPresenter();
|
||||
_craftingButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().CraftingButton;
|
||||
_craftingButton.OnToggled += _presenter.OnHudCraftingButtonToggled;
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_presenter == null)
|
||||
return;
|
||||
_craftingButton!.Pressed = false;
|
||||
_craftingButton!.OnToggled -= _presenter.OnHudCraftingButtonToggled;
|
||||
_craftingButton = null;
|
||||
_presenter.Dispose();
|
||||
_presenter = null;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.Links;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Input;
|
||||
@@ -20,18 +20,20 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
[Dependency] private readonly IUriOpener _uri = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
private Options.UI.EscapeMenu? _escapeWindow;
|
||||
|
||||
private MenuButton? _escapeButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_escapeWindow == null);
|
||||
_gameHud.EscapeButtonToggled += GameHudOnEscapeButtonToggled;
|
||||
_escapeButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().EscapeButton;
|
||||
_escapeButton.OnPressed += EscapeButtonOnOnPressed;
|
||||
|
||||
_escapeWindow = UIManager.CreateWindow<Options.UI.EscapeMenu>();
|
||||
_escapeWindow.OnClose += () => { _gameHud.EscapeButtonDown = false; };
|
||||
_escapeWindow.OnOpen += () => { _gameHud.EscapeButtonDown = true; };
|
||||
_escapeWindow.OnClose += () => { _escapeButton.Pressed = false; };
|
||||
_escapeWindow.OnOpen += () => { _escapeButton.Pressed = true; };
|
||||
|
||||
_escapeWindow.ChangelogButton.OnPressed += _ =>
|
||||
{
|
||||
@@ -43,7 +45,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
_escapeWindow.RulesButton.OnPressed += _ =>
|
||||
{
|
||||
CloseEscapeWindow();
|
||||
new RulesAndInfoWindow().Open();
|
||||
UIManager.GetUIController<InfoUIController>().OpenWindow();
|
||||
};
|
||||
|
||||
_escapeWindow.DisconnectButton.OnPressed += _ =>
|
||||
@@ -75,11 +77,6 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
.Register<EscapeUIController>();
|
||||
}
|
||||
|
||||
private void GameHudOnEscapeButtonToggled(bool obj)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_escapeWindow != null)
|
||||
@@ -87,12 +84,22 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
_escapeWindow.Dispose();
|
||||
_escapeWindow = null;
|
||||
}
|
||||
_gameHud.EscapeButtonToggled -= GameHudOnEscapeButtonToggled;
|
||||
_gameHud.EscapeButtonDown = false;
|
||||
|
||||
if (_escapeButton != null)
|
||||
{
|
||||
_escapeButton.OnPressed -= EscapeButtonOnOnPressed;
|
||||
_escapeButton.Pressed = false;
|
||||
_escapeButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<EscapeUIController>();
|
||||
}
|
||||
|
||||
private void EscapeButtonOnOnPressed(ButtonEventArgs obj)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void CloseEscapeWindow()
|
||||
{
|
||||
_escapeWindow?.Close();
|
||||
@@ -110,6 +117,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
else
|
||||
{
|
||||
_escapeWindow.OpenCentered();
|
||||
_escapeButton!.Pressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostTargetWindow : DefaultWindow
|
||||
{
|
||||
private readonly IEntityNetworkManager _netManager;
|
||||
|
||||
private List<(string, EntityUid)> _warps = new();
|
||||
|
||||
public GhostTargetWindow(IEntityNetworkManager netManager)
|
||||
public event Action<EntityUid>? WarpClicked;
|
||||
|
||||
public GhostTargetWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_netManager = netManager;
|
||||
}
|
||||
|
||||
public void UpdateWarps(IEnumerable<GhostWarp> warps)
|
||||
@@ -47,7 +45,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
||||
|
||||
private void AddButtons()
|
||||
{
|
||||
foreach (var (name, warp) in _warps)
|
||||
foreach (var (name, warpTarget) in _warps)
|
||||
{
|
||||
var currentButtonRef = new Button
|
||||
{
|
||||
@@ -60,11 +58,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
|
||||
ClipText = true,
|
||||
};
|
||||
|
||||
currentButtonRef.OnPressed += _ =>
|
||||
{
|
||||
var msg = new GhostWarpToTargetRequestEvent(warp);
|
||||
_netManager.SendSystemNetworkMessage(msg);
|
||||
};
|
||||
currentButtonRef.OnPressed += _ => WarpClicked?.Invoke(warpTarget);
|
||||
|
||||
ButtonContainer.AddChild(currentButtonRef);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
@@ -4,8 +4,6 @@ using Content.Shared.Ghost.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.GameObjects;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
using Content.Client.Ghost;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Systems.Ghost.Controls;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost
|
||||
{
|
||||
public sealed class GhostGui : Control
|
||||
{
|
||||
private readonly Button _returnToBody = new() {Text = Loc.GetString("ghost-gui-return-to-body-button") };
|
||||
private readonly Button _ghostWarp = new() {Text = Loc.GetString("ghost-gui-ghost-warp-button") };
|
||||
private readonly Button _ghostRoles = new();
|
||||
private readonly GhostComponent _owner;
|
||||
private readonly GhostSystem _system;
|
||||
|
||||
public GhostTargetWindow? TargetWindow { get; }
|
||||
|
||||
public GhostGui(GhostComponent owner, GhostSystem system, IEntityNetworkManager eventBus)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_owner = owner;
|
||||
_system = system;
|
||||
|
||||
TargetWindow = new GhostTargetWindow(eventBus);
|
||||
|
||||
MouseFilter = MouseFilterMode.Ignore;
|
||||
|
||||
_ghostWarp.OnPressed += _ =>
|
||||
{
|
||||
eventBus.SendSystemNetworkMessage(new GhostWarpsRequestEvent());
|
||||
TargetWindow.Populate();
|
||||
TargetWindow.OpenCentered();
|
||||
};
|
||||
_returnToBody.OnPressed += _ =>
|
||||
{
|
||||
var msg = new GhostReturnToBodyRequest();
|
||||
eventBus.SendSystemNetworkMessage(msg);
|
||||
};
|
||||
_ghostRoles.OnPressed += _ => IoCManager.Resolve<IClientConsoleHost>()
|
||||
.RemoteExecuteCommand(null, "ghostroles");
|
||||
|
||||
AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
_returnToBody,
|
||||
_ghostWarp,
|
||||
_ghostRoles,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
_returnToBody.Disabled = !_owner.CanReturnToBody;
|
||||
_ghostRoles.Text = Loc.GetString("ghost-gui-ghost-roles-button", ("count", _system.AvailableGhostRoleCount));
|
||||
if (_system.AvailableGhostRoleCount != 0)
|
||||
{
|
||||
_ghostRoles.StyleClasses.Add(StyleBase.ButtonCaution);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ghostRoles.StyleClasses.Remove(StyleBase.ButtonCaution);
|
||||
}
|
||||
TargetWindow?.Populate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
TargetWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs
Normal file
130
Content.Client/UserInterface/Systems/Ghost/GhostUIController.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Ghost;
|
||||
using Content.Client.UserInterface.Systems.Ghost.Widgets;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost;
|
||||
|
||||
// TODO hud refactor BEFORE MERGE fix ghost gui being too far up
|
||||
public sealed class GhostUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<GhostSystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityNetworkManager _net = default!;
|
||||
|
||||
[UISystemDependency] private readonly GhostSystem? _system = default;
|
||||
|
||||
private GhostGui? Gui => UIManager.GetActiveUIWidgetOrNull<GhostGui>();
|
||||
|
||||
public void OnSystemLoaded(GhostSystem system)
|
||||
{
|
||||
system.PlayerRemoved += OnPlayerRemoved;
|
||||
system.PlayerUpdated += OnPlayerUpdated;
|
||||
system.PlayerAttached += OnPlayerAttached;
|
||||
system.PlayerDetached += OnPlayerDetached;
|
||||
system.GhostWarpsResponse += OnWarpsResponse;
|
||||
system.GhostRoleCountUpdated += OnRoleCountUpdated;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(GhostSystem system)
|
||||
{
|
||||
system.PlayerRemoved -= OnPlayerRemoved;
|
||||
system.PlayerUpdated -= OnPlayerUpdated;
|
||||
system.PlayerAttached -= OnPlayerAttached;
|
||||
system.PlayerDetached -= OnPlayerDetached;
|
||||
system.GhostWarpsResponse -= OnWarpsResponse;
|
||||
system.GhostRoleCountUpdated -= OnRoleCountUpdated;
|
||||
}
|
||||
|
||||
private void UpdateGui()
|
||||
{
|
||||
Gui?.Update(_system?.AvailableGhostRoleCount, _system?.Player?.CanReturnToBody);
|
||||
}
|
||||
|
||||
private void OnPlayerRemoved(GhostComponent component)
|
||||
{
|
||||
Gui?.Hide();
|
||||
}
|
||||
|
||||
private void OnPlayerUpdated(GhostComponent component)
|
||||
{
|
||||
UpdateGui();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(GhostComponent component)
|
||||
{
|
||||
if (Gui == null)
|
||||
return;
|
||||
|
||||
Gui.Visible = true;
|
||||
UpdateGui();
|
||||
}
|
||||
|
||||
private void OnPlayerDetached()
|
||||
{
|
||||
Gui?.Hide();
|
||||
}
|
||||
|
||||
private void OnWarpsResponse(GhostWarpsResponseEvent msg)
|
||||
{
|
||||
if (Gui?.TargetWindow is not { } window)
|
||||
return;
|
||||
|
||||
window.UpdateWarps(msg.Warps);
|
||||
window.Populate();
|
||||
}
|
||||
|
||||
private void OnRoleCountUpdated(GhostUpdateGhostRoleCountEvent msg)
|
||||
{
|
||||
UpdateGui();
|
||||
}
|
||||
|
||||
private void OnWarpClicked(EntityUid player)
|
||||
{
|
||||
var msg = new GhostWarpToTargetRequestEvent(player);
|
||||
_net.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
if (Gui == null)
|
||||
return;
|
||||
|
||||
Gui.RequestWarpsPressed += RequestWarps;
|
||||
Gui.ReturnToBodyPressed += ReturnToBody;
|
||||
Gui.GhostRolesPressed += GhostRolesPressed;
|
||||
Gui.TargetWindow.WarpClicked += OnWarpClicked;
|
||||
|
||||
Gui.Visible = _system?.IsGhost ?? false;
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (Gui == null)
|
||||
return;
|
||||
|
||||
Gui.RequestWarpsPressed -= RequestWarps;
|
||||
Gui.ReturnToBodyPressed -= ReturnToBody;
|
||||
Gui.GhostRolesPressed -= GhostRolesPressed;
|
||||
Gui.TargetWindow.WarpClicked -= OnWarpClicked;
|
||||
|
||||
Gui.Hide();
|
||||
}
|
||||
|
||||
private void ReturnToBody()
|
||||
{
|
||||
_system?.ReturnToBody();
|
||||
}
|
||||
|
||||
private void RequestWarps()
|
||||
{
|
||||
_system?.RequestWarps();
|
||||
Gui?.TargetWindow.Populate();
|
||||
Gui?.TargetWindow.OpenCentered();
|
||||
}
|
||||
|
||||
private void GhostRolesPressed()
|
||||
{
|
||||
_system?.OpenGhostRoles();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<widgets:GhostGui xmlns="https://spacestation14.io"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Ghost.Widgets"
|
||||
HorizontalAlignment="Center">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="ReturnToBodyButton" Text="{Loc ghost-gui-return-to-body-button}" />
|
||||
<Button Name="GhostWarpButton" Text="{Loc ghost-gui-ghost-warp-button}" />
|
||||
<Button Name="GhostRolesButton" />
|
||||
</BoxContainer>
|
||||
</widgets:GhostGui>
|
||||
@@ -0,0 +1,66 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Systems.Ghost.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostGui : UIWidget
|
||||
{
|
||||
public GhostTargetWindow TargetWindow { get; }
|
||||
|
||||
public event Action? RequestWarpsPressed;
|
||||
public event Action? ReturnToBodyPressed;
|
||||
public event Action? GhostRolesPressed;
|
||||
|
||||
public GhostGui()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
TargetWindow = new GhostTargetWindow();
|
||||
|
||||
MouseFilter = MouseFilterMode.Ignore;
|
||||
|
||||
GhostWarpButton.OnPressed += _ => RequestWarpsPressed?.Invoke();
|
||||
ReturnToBodyButton.OnPressed += _ => ReturnToBodyPressed?.Invoke();
|
||||
GhostRolesButton.OnPressed += _ => GhostRolesPressed?.Invoke();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
TargetWindow.Close();
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
public void Update(int? roles, bool? canReturnToBody)
|
||||
{
|
||||
ReturnToBodyButton.Disabled = !canReturnToBody ?? true;
|
||||
|
||||
if (roles != null)
|
||||
{
|
||||
GhostRolesButton.Text = Loc.GetString("ghost-gui-ghost-roles-button", ("count", roles));
|
||||
if (roles > 0)
|
||||
{
|
||||
GhostRolesButton.StyleClasses.Add(StyleBase.ButtonCaution);
|
||||
}
|
||||
else
|
||||
{
|
||||
GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
|
||||
}
|
||||
}
|
||||
|
||||
TargetWindow.Populate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
TargetWindow.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
|
||||
public sealed class HandButton : SlotControl
|
||||
{
|
||||
public HandButton(string handName, HandLocation handLocation)
|
||||
{
|
||||
Name = "hand_" + handName;
|
||||
SlotName = handName;
|
||||
SetBackground(handLocation);
|
||||
}
|
||||
|
||||
private void SetBackground(HandLocation handLoc)
|
||||
{
|
||||
ButtonTexturePath = handLoc switch
|
||||
{
|
||||
HandLocation.Left => "Slots/hand_l",
|
||||
HandLocation.Middle => "Slots/hand_m",
|
||||
HandLocation.Right => "Slots/hand_r",
|
||||
_ => ButtonTexturePath
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
|
||||
public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
|
||||
{
|
||||
private readonly GridContainer _grid;
|
||||
public int ColumnLimit { get => _grid.Columns; set => _grid.Columns = value; }
|
||||
public int MaxButtonCount { get; set; } = 0;
|
||||
|
||||
public HandsContainer()
|
||||
{
|
||||
AddChild(_grid = new GridContainer());
|
||||
_grid.ExpandBackwards = true;
|
||||
}
|
||||
|
||||
public override HandButton? AddButton(HandButton newButton)
|
||||
{
|
||||
if (MaxButtonCount > 0)
|
||||
{
|
||||
if (ButtonCount >= MaxButtonCount)
|
||||
return null;
|
||||
|
||||
_grid.AddChild(newButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
_grid.AddChild(newButton);
|
||||
}
|
||||
|
||||
return base.AddButton(newButton);
|
||||
}
|
||||
|
||||
public override void RemoveButton(string handName)
|
||||
{
|
||||
var button = GetButton(handName);
|
||||
if (button == null)
|
||||
return;
|
||||
base.RemoveButton(button);
|
||||
_grid.RemoveChild(button);
|
||||
}
|
||||
|
||||
public bool TryGetLastButton(out HandButton? control)
|
||||
{
|
||||
if (Buttons.Count == 0)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
control = Buttons.Values.Last();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRemoveLastHand(out HandButton? control)
|
||||
{
|
||||
var success = TryGetLastButton(out control);
|
||||
if (control != null)
|
||||
RemoveButton(control);
|
||||
return success;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearButtons();
|
||||
_grid.DisposeAllChildren();
|
||||
}
|
||||
|
||||
public IEnumerable<HandButton> GetButtons()
|
||||
{
|
||||
foreach (var child in _grid.Children)
|
||||
{
|
||||
if (child is HandButton hand)
|
||||
yield return hand;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFull => (MaxButtonCount != 0 && ButtonCount >= MaxButtonCount);
|
||||
|
||||
public int ButtonCount => _grid.ChildCount;
|
||||
}
|
||||
334
Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
Normal file
334
Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.Hands.Systems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
using Content.Client.UserInterface.Systems.Hotbar.Widgets;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hands;
|
||||
|
||||
public sealed class HandsUIController : UIController, IOnStateEntered<GameplayState>, IOnSystemChanged<HandsSystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
[UISystemDependency] private readonly HandsSystem _handsSystem = default!;
|
||||
|
||||
private readonly List<HandsContainer> _handsContainers = new();
|
||||
private readonly Dictionary<string, int> _handContainerIndices = new();
|
||||
private readonly Dictionary<string, HandButton> _handLookup = new();
|
||||
private HandsComponent? _playerHandsComponent;
|
||||
private HandButton? _activeHand = null;
|
||||
private int _backupSuffix = 0; //this is used when autogenerating container names if they don't have names
|
||||
|
||||
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
|
||||
|
||||
public void OnSystemLoaded(HandsSystem system)
|
||||
{
|
||||
_handsSystem.OnPlayerAddHand += OnAddHand;
|
||||
_handsSystem.OnPlayerItemAdded += OnItemAdded;
|
||||
_handsSystem.OnPlayerItemRemoved += OnItemRemoved;
|
||||
_handsSystem.OnPlayerSetActiveHand += SetActiveHand;
|
||||
_handsSystem.OnPlayerRemoveHand += RemoveHand;
|
||||
_handsSystem.OnPlayerHandsAdded += LoadPlayerHands;
|
||||
_handsSystem.OnPlayerHandsRemoved += UnloadPlayerHands;
|
||||
_handsSystem.OnPlayerHandBlocked += HandBlocked;
|
||||
_handsSystem.OnPlayerHandUnblocked += HandUnblocked;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(HandsSystem system)
|
||||
{
|
||||
_handsSystem.OnPlayerAddHand -= OnAddHand;
|
||||
_handsSystem.OnPlayerItemAdded -= OnItemAdded;
|
||||
_handsSystem.OnPlayerItemRemoved -= OnItemRemoved;
|
||||
_handsSystem.OnPlayerSetActiveHand -= SetActiveHand;
|
||||
_handsSystem.OnPlayerRemoveHand -= RemoveHand;
|
||||
_handsSystem.OnPlayerHandsAdded -= LoadPlayerHands;
|
||||
_handsSystem.OnPlayerHandsRemoved -= UnloadPlayerHands;
|
||||
_handsSystem.OnPlayerHandBlocked -= HandBlocked;
|
||||
_handsSystem.OnPlayerHandUnblocked -= HandUnblocked;
|
||||
}
|
||||
|
||||
private void OnAddHand(string name, HandLocation location)
|
||||
{
|
||||
AddHand(name, location);
|
||||
}
|
||||
|
||||
private void HandPressed(GUIBoundKeyEventArgs args, SlotControl hand)
|
||||
{
|
||||
if (_playerHandsComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_handsSystem.UIInventoryExamine(hand.SlotName);
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UseSecondary)
|
||||
{
|
||||
_handsSystem.UIHandOpenContextMenu(hand.SlotName);
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_handsSystem.UIHandClick(_playerHandsComponent, hand.SlotName);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadPlayerHands()
|
||||
{
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = false;
|
||||
|
||||
_handContainerIndices.Clear();
|
||||
_handLookup.Clear();
|
||||
_playerHandsComponent = null;
|
||||
|
||||
foreach (var container in _handsContainers)
|
||||
{
|
||||
container.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPlayerHands(HandsComponent handsComp)
|
||||
{
|
||||
DebugTools.Assert(_playerHandsComponent == null);
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = true;
|
||||
|
||||
_playerHandsComponent = handsComp;
|
||||
foreach (var (name, hand) in handsComp.Hands)
|
||||
{
|
||||
var handButton = AddHand(name, hand.Location);
|
||||
handButton.SpriteView.Sprite = _entities.GetComponentOrNull<SpriteComponent>(hand.HeldEntity);
|
||||
}
|
||||
|
||||
var activeHand = handsComp.ActiveHand;
|
||||
if (activeHand == null)
|
||||
return;
|
||||
SetActiveHand(activeHand.Name);
|
||||
}
|
||||
|
||||
private void HandBlocked(string handName)
|
||||
{
|
||||
if (!_handLookup.TryGetValue(handName, out var hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hand.Blocked = true;
|
||||
}
|
||||
|
||||
private void HandUnblocked(string handName)
|
||||
{
|
||||
if (!_handLookup.TryGetValue(handName, out var hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hand.Blocked = false;
|
||||
}
|
||||
|
||||
private int GetHandContainerIndex(string containerName)
|
||||
{
|
||||
if (!_handContainerIndices.TryGetValue(containerName, out var result))
|
||||
return -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnItemAdded(string name, EntityUid entity)
|
||||
{
|
||||
HandsGui?.UpdatePanelEntity(entity);
|
||||
var hand = GetHand(name);
|
||||
if (hand == null)
|
||||
return;
|
||||
if (_entities.TryGetComponent(entity, out ISpriteComponent? sprite))
|
||||
{
|
||||
hand.SpriteView.Sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemRemoved(string name, EntityUid entity)
|
||||
{
|
||||
HandsGui?.UpdatePanelEntity(null);
|
||||
var hand = GetHand(name);
|
||||
if (hand == null)
|
||||
return;
|
||||
hand.SpriteView.Sprite = null;
|
||||
}
|
||||
|
||||
private HandsContainer GetFirstAvailableContainer()
|
||||
{
|
||||
if (_handsContainers.Count == 0)
|
||||
throw new Exception("Could not find an attached hand hud container");
|
||||
foreach (var container in _handsContainers)
|
||||
{
|
||||
if (container.IsFull)
|
||||
continue;
|
||||
return container;
|
||||
}
|
||||
|
||||
throw new Exception("All attached hand hud containers were full!");
|
||||
}
|
||||
|
||||
public bool TryGetHandContainer(string containerName, out HandsContainer? container)
|
||||
{
|
||||
container = null;
|
||||
var containerIndex = GetHandContainerIndex(containerName);
|
||||
if (containerIndex == -1)
|
||||
return false;
|
||||
container = _handsContainers[containerIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
//propagate hand activation to the hand system.
|
||||
private void StorageActivate(GUIBoundKeyEventArgs args, SlotControl handControl)
|
||||
{
|
||||
_handsSystem.UIHandActivate(handControl.SlotName);
|
||||
}
|
||||
|
||||
private void SetActiveHand(string? handName)
|
||||
{
|
||||
if (handName == null)
|
||||
{
|
||||
if (_activeHand != null)
|
||||
_activeHand.Highlight = false;
|
||||
|
||||
HandsGui?.UpdatePanelEntity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_handLookup.TryGetValue(handName, out var handControl) || handControl == _activeHand)
|
||||
return;
|
||||
|
||||
if (_activeHand != null)
|
||||
_activeHand.Highlight = false;
|
||||
|
||||
handControl.Highlight = true;
|
||||
_activeHand = handControl;
|
||||
|
||||
if (HandsGui != null &&
|
||||
_playerHandsComponent != null &&
|
||||
_playerHandsComponent.Hands.TryGetValue(handName, out var hand))
|
||||
{
|
||||
HandsGui.UpdatePanelEntity(hand.HeldEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private HandButton? GetHand(string handName)
|
||||
{
|
||||
_handLookup.TryGetValue(handName, out var handControl);
|
||||
return handControl;
|
||||
}
|
||||
|
||||
private HandButton AddHand(string handName, HandLocation location)
|
||||
{
|
||||
var button = new HandButton(handName, location);
|
||||
button.StoragePressed += StorageActivate;
|
||||
button.Pressed += HandPressed;
|
||||
|
||||
if (!_handLookup.TryAdd(handName, button))
|
||||
throw new Exception("Tried to add hand with duplicate name to UI. Name:" + handName);
|
||||
|
||||
GetFirstAvailableContainer().AddButton(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void RemoveHand(string handName)
|
||||
{
|
||||
RemoveHand(handName, out _);
|
||||
}
|
||||
|
||||
private bool RemoveHand(string handName, out HandButton? handButton)
|
||||
{
|
||||
handButton = null;
|
||||
if (!_handLookup.TryGetValue(handName, out handButton))
|
||||
return false;
|
||||
if (handButton.Parent is HandsContainer handContainer)
|
||||
{
|
||||
handContainer.RemoveButton(handButton);
|
||||
}
|
||||
|
||||
_handLookup.Remove(handName);
|
||||
handButton.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public string RegisterHandContainer(HandsContainer handContainer)
|
||||
{
|
||||
var name = "HandContainer_" + _backupSuffix;
|
||||
;
|
||||
if (handContainer.Name == null)
|
||||
{
|
||||
handContainer.Name = name;
|
||||
_backupSuffix++;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = handContainer.Name;
|
||||
}
|
||||
|
||||
_handContainerIndices.Add(name, _handsContainers.Count);
|
||||
_handsContainers.Add(handContainer);
|
||||
return name;
|
||||
}
|
||||
|
||||
public bool RemoveHandContainer(string handContainerName)
|
||||
{
|
||||
var index = GetHandContainerIndex(handContainerName);
|
||||
if (index == -1)
|
||||
return false;
|
||||
_handContainerIndices.Remove(handContainerName);
|
||||
_handsContainers.RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveHandContainer(string handContainerName, out HandsContainer? container)
|
||||
{
|
||||
var success = _handContainerIndices.TryGetValue(handContainerName, out var index);
|
||||
container = _handsContainers[index];
|
||||
_handContainerIndices.Remove(handContainerName);
|
||||
_handsContainers.RemoveAt(index);
|
||||
return success;
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = _playerHandsComponent != null;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
// TODO this should be event based but 2 systems modify the same component differently for some reason
|
||||
foreach (var container in _handsContainers)
|
||||
{
|
||||
foreach (var hand in container.GetButtons())
|
||||
{
|
||||
if (hand.Entity is not { } entity)
|
||||
return;
|
||||
|
||||
if (_entities.Deleted(entity) ||
|
||||
!_entities.TryGetComponent(entity, out ItemCooldownComponent? cooldown) ||
|
||||
cooldown is not { CooldownStart: { } start, CooldownEnd: { } end})
|
||||
{
|
||||
hand.CooldownDisplay.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hand.CooldownDisplay.FromTime(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Content.Client.UserInterface.Systems.Hands;
|
||||
using Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
using Content.Client.UserInterface.Systems.Inventory;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hotbar;
|
||||
|
||||
public sealed class HotbarUIController : UIController
|
||||
{
|
||||
private InventoryUIController? _inventory;
|
||||
private HandsUIController? _hands;
|
||||
|
||||
public void Setup(HandsContainer handsContainer, ItemSlotButtonContainer inventoryBar, ItemStatusPanel handStatus)
|
||||
{
|
||||
_inventory = UIManager.GetUIController<InventoryUIController>();
|
||||
_hands = UIManager.GetUIController<HandsUIController>();
|
||||
_hands.RegisterHandContainer(handsContainer);
|
||||
_inventory.RegisterInventoryBarContainer(inventoryBar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<widgets:HotbarGui
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
|
||||
xmlns:hands="clr-namespace:Content.Client.UserInterface.Systems.Hands.Controls"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
|
||||
Name="HotbarInterface"
|
||||
VerticalExpand="False"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<inventory:ItemStatusPanel
|
||||
Name="StatusPanel"
|
||||
Visible="False"
|
||||
HorizontalAlignment="Center"
|
||||
/>
|
||||
<inventory:ItemSlotButtonContainer
|
||||
Name="InventoryHotbar"
|
||||
Visible="False"
|
||||
Columns="10"
|
||||
SlotGroup="Default"
|
||||
ExpandBackwards="True"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
/>
|
||||
<BoxContainer Orientation="Horizontal" Name="Hotbar">
|
||||
<inventory:ItemSlotButtonContainer
|
||||
Name="SecondHotbar"
|
||||
SlotGroup="SecondHotbar"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalExpand="False"
|
||||
Columns="6"
|
||||
HorizontalExpand="True"/>
|
||||
<hands:HandsContainer
|
||||
Name="HandContainer"
|
||||
Access="Protected"
|
||||
HorizontalAlignment="Center"
|
||||
ColumnLimit="6" />
|
||||
<inventory:ItemSlotButtonContainer
|
||||
Name="MainHotbar"
|
||||
SlotGroup="MainHotbar"
|
||||
VerticalExpand="False"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalExpand="True"
|
||||
Columns="6"
|
||||
/>
|
||||
</BoxContainer>
|
||||
</widgets:HotbarGui>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hotbar.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HotbarGui : UIWidget
|
||||
{
|
||||
public HotbarGui()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
StatusPanel.Update(null);
|
||||
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
|
||||
|
||||
hotbarController.Setup(HandContainer, InventoryHotbar, StatusPanel);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
|
||||
}
|
||||
|
||||
public void UpdatePanelEntity(EntityUid? entity)
|
||||
{
|
||||
StatusPanel.Update(entity);
|
||||
if (entity == null)
|
||||
{
|
||||
StatusPanel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
StatusPanel.Visible = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Info;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Info;
|
||||
|
||||
public sealed class InfoUIController : UIController, IOnStateExited<GameplayState>
|
||||
{
|
||||
private RulesAndInfoWindow? _infoWindow;
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_infoWindow == null)
|
||||
return;
|
||||
|
||||
_infoWindow.Dispose();
|
||||
_infoWindow = null;
|
||||
}
|
||||
|
||||
public void OpenWindow()
|
||||
{
|
||||
if (_infoWindow == null || _infoWindow.Disposed)
|
||||
{
|
||||
_infoWindow = UIManager.CreateWindow<RulesAndInfoWindow>();
|
||||
}
|
||||
|
||||
_infoWindow?.OpenCentered();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
|
||||
public sealed class InventoryDisplay : LayoutContainer
|
||||
{
|
||||
private int Columns = 0;
|
||||
private int Rows = 0;
|
||||
private const int MarginThickness = 10;
|
||||
private const int ButtonSpacing = 5;
|
||||
private const int ButtonSize = 75;
|
||||
private readonly Control resizer;
|
||||
|
||||
private readonly Dictionary<string, (SlotControl, Vector2i)> _buttons = new();
|
||||
|
||||
public InventoryDisplay()
|
||||
{
|
||||
resizer = new Control();
|
||||
AddChild(resizer);
|
||||
}
|
||||
|
||||
public SlotControl? AddButton(SlotControl newButton, Vector2i buttonOffset)
|
||||
{
|
||||
AddChild(newButton);
|
||||
HorizontalExpand = true;
|
||||
VerticalExpand = true;
|
||||
InheritChildMeasure = true;
|
||||
if (!_buttons.TryAdd(newButton.SlotName, (newButton, buttonOffset)))
|
||||
Logger.Warning("Tried to add button without a slot!");
|
||||
SetPosition(newButton, buttonOffset * ButtonSize + new Vector2(ButtonSpacing, ButtonSpacing));
|
||||
UpdateSizeData(buttonOffset);
|
||||
return newButton;
|
||||
}
|
||||
|
||||
public SlotControl? GetButton(string slotName)
|
||||
{
|
||||
return !_buttons.TryGetValue(slotName, out var foundButton) ? null : foundButton.Item1;
|
||||
}
|
||||
|
||||
private void UpdateSizeData(Vector2i buttonOffset)
|
||||
{
|
||||
var (x, _) = buttonOffset;
|
||||
if (x > Columns)
|
||||
Columns = x;
|
||||
var (_, y) = buttonOffset;
|
||||
if (y > Rows)
|
||||
Rows = y;
|
||||
resizer.SetHeight = (Rows + 1) * (ButtonSize + ButtonSpacing);
|
||||
resizer.SetWidth = (Columns + 1) * (ButtonSize + ButtonSpacing);
|
||||
}
|
||||
|
||||
public bool TryGetButton(string slotName, out SlotControl? button)
|
||||
{
|
||||
var success = _buttons.TryGetValue(slotName, out var buttonData);
|
||||
button = buttonData.Item1;
|
||||
return success;
|
||||
}
|
||||
|
||||
public void RemoveButton(string slotName)
|
||||
{
|
||||
if (!_buttons.Remove(slotName))
|
||||
return;
|
||||
//recalculate the size of the control when a slot is removed
|
||||
Columns = 0;
|
||||
Rows = 0;
|
||||
foreach (var (_, (_, buttonOffset)) in _buttons)
|
||||
{
|
||||
UpdateSizeData(buttonOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearButtons()
|
||||
{
|
||||
Children.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
|
||||
public sealed class ItemSlotButtonContainer : ItemSlotUIContainer<SlotControl>
|
||||
{
|
||||
private readonly InventoryUIController _inventoryController;
|
||||
private string _slotGroup = "";
|
||||
|
||||
public string SlotGroup
|
||||
{
|
||||
get => _slotGroup;
|
||||
set
|
||||
{
|
||||
_inventoryController.RemoveSlotGroup(SlotGroup);
|
||||
_slotGroup = value;
|
||||
_inventoryController.RegisterSlotGroupContainer(this);
|
||||
}
|
||||
}
|
||||
|
||||
public ItemSlotButtonContainer()
|
||||
{
|
||||
_inventoryController = UserInterfaceManager.GetUIController<InventoryUIController>();
|
||||
}
|
||||
|
||||
~ItemSlotButtonContainer()
|
||||
{
|
||||
_inventoryController.RemoveSlotGroup(SlotGroup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
|
||||
public interface IItemslotUIContainer
|
||||
{
|
||||
public bool TryRegisterButton(SlotControl control, string newSlotName);
|
||||
|
||||
public bool TryAddButton(SlotControl control);
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContainer where T : SlotControl
|
||||
{
|
||||
protected readonly Dictionary<string, T> Buttons = new();
|
||||
|
||||
public virtual bool TryAddButton(T newButton, out T button)
|
||||
{
|
||||
var tempButton = AddButton(newButton);
|
||||
if (tempButton == null)
|
||||
{
|
||||
button = newButton;
|
||||
return false;
|
||||
}
|
||||
|
||||
button = newButton;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearButtons()
|
||||
{
|
||||
foreach (var button in Buttons.Values)
|
||||
{
|
||||
button.Dispose();
|
||||
}
|
||||
|
||||
Buttons.Clear();
|
||||
}
|
||||
|
||||
public bool TryRegisterButton(SlotControl control, string newSlotName)
|
||||
{
|
||||
if (newSlotName == "")
|
||||
return false;
|
||||
if (!(control is T slotButton))
|
||||
return false;
|
||||
if (Buttons.TryGetValue(newSlotName, out var foundButton))
|
||||
{
|
||||
if (control == foundButton)
|
||||
return true; //if the slotName is already set do nothing
|
||||
throw new Exception("Could not update button to slot:" + newSlotName + " slot already assigned!");
|
||||
}
|
||||
|
||||
Buttons.Remove(slotButton.SlotName);
|
||||
AddButton(slotButton);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAddButton(SlotControl control)
|
||||
{
|
||||
if (control is not T newButton)
|
||||
return false;
|
||||
return AddButton(newButton) != null;
|
||||
}
|
||||
|
||||
public virtual T? AddButton(T newButton)
|
||||
{
|
||||
if (!Children.Contains(newButton) && newButton.Parent == null && newButton.SlotName != "")
|
||||
AddChild(newButton);
|
||||
return AddButtonToDict(newButton);
|
||||
}
|
||||
|
||||
protected virtual T? AddButtonToDict(T newButton)
|
||||
{
|
||||
if (newButton.SlotName == "")
|
||||
{
|
||||
Logger.Warning("Could not add button " + newButton.Name + "No slotname");
|
||||
}
|
||||
|
||||
return !Buttons.TryAdd(newButton.SlotName, newButton) ? null : newButton;
|
||||
}
|
||||
|
||||
public virtual void RemoveButton(string slotName)
|
||||
{
|
||||
if (!Buttons.TryGetValue(slotName, out var button))
|
||||
return;
|
||||
RemoveButton(button);
|
||||
}
|
||||
|
||||
public virtual void RemoveButtons(params string[] slotNames)
|
||||
{
|
||||
foreach (var slotName in slotNames)
|
||||
{
|
||||
RemoveButton(slotName);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void RemoveButtons(params T?[] buttons)
|
||||
{
|
||||
foreach (var button in buttons)
|
||||
{
|
||||
if (button != null)
|
||||
RemoveButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void RemoveButtonFromDict(T button)
|
||||
{
|
||||
Buttons.Remove(button.SlotName);
|
||||
}
|
||||
|
||||
public virtual void RemoveButton(T button)
|
||||
{
|
||||
RemoveButtonFromDict(button);
|
||||
Children.Remove(button);
|
||||
button.Dispose();
|
||||
}
|
||||
|
||||
public virtual T? GetButton(string slotName)
|
||||
{
|
||||
return !Buttons.TryGetValue(slotName, out var button) ? null : button;
|
||||
}
|
||||
|
||||
public virtual bool TryGetButton(string slotName, [NotNullWhen(true)] out T? button)
|
||||
{
|
||||
return (button = GetButton(slotName)) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<controls:ItemStatusPanel
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center"
|
||||
MinSize="150 0">
|
||||
<PanelContainer
|
||||
Name="Panel"
|
||||
ModulateSelfOverride="#FFFFFFE6"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxTexture
|
||||
ContentMarginLeftOverride="6"
|
||||
ContentMarginRightOverride="6"
|
||||
ContentMarginTopOverride="4"
|
||||
ContentMarginBottomOverride="4" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="0">
|
||||
<BoxContainer Name="StatusContents" Orientation="Vertical"/>
|
||||
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:ItemStatusPanel>
|
||||
@@ -0,0 +1,138 @@
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.IoC.StaticIoC;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ItemStatusPanel : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
[ViewVariables] private EntityUid? _entity;
|
||||
|
||||
public ItemStatusPanel()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
SetSide(HandLocation.Middle);
|
||||
}
|
||||
|
||||
public void SetSide(HandLocation location)
|
||||
{
|
||||
string texture;
|
||||
StyleBox.Margin cutOut;
|
||||
StyleBox.Margin flat;
|
||||
Label.AlignMode textAlign;
|
||||
|
||||
switch (location)
|
||||
{
|
||||
case HandLocation.Left:
|
||||
texture = "/Textures/Interface/Nano/item_status_right.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Left | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Right | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Right;
|
||||
break;
|
||||
case HandLocation.Middle:
|
||||
texture = "/Textures/Interface/Nano/item_status_middle.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Left;
|
||||
break;
|
||||
case HandLocation.Right:
|
||||
texture = "/Textures/Interface/Nano/item_status_left.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Left;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(location), location, null);
|
||||
}
|
||||
|
||||
var panel = (StyleBoxTexture) Panel.PanelOverride!;
|
||||
panel.Texture = ResC.GetTexture(texture);
|
||||
panel.SetPatchMargin(flat, 2);
|
||||
panel.SetPatchMargin(cutOut, 13);
|
||||
|
||||
ItemNameLabel.Align = textAlign;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateItemName();
|
||||
}
|
||||
|
||||
public void Update(EntityUid? entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
ClearOldStatus();
|
||||
_entity = null;
|
||||
Panel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity != _entity)
|
||||
{
|
||||
_entity = entity.Value;
|
||||
BuildNewEntityStatus();
|
||||
|
||||
UpdateItemName();
|
||||
}
|
||||
|
||||
Panel.Visible = true;
|
||||
}
|
||||
|
||||
private void UpdateItemName()
|
||||
{
|
||||
if (_entity == null)
|
||||
return;
|
||||
|
||||
if (!_entityManager.TryGetComponent<MetaDataComponent>(_entity, out var meta) || meta.Deleted)
|
||||
{
|
||||
Update(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entityManager.TryGetComponent(_entity, out HandVirtualItemComponent? virtualItem)
|
||||
&& _entityManager.EntityExists(virtualItem.BlockingEntity))
|
||||
{
|
||||
// Uses identity because we can be blocked by pulling someone
|
||||
ItemNameLabel.Text = Identity.Name(virtualItem.BlockingEntity, _entityManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemNameLabel.Text = Identity.Name(_entity.Value, _entityManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearOldStatus()
|
||||
{
|
||||
StatusContents.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void BuildNewEntityStatus()
|
||||
{
|
||||
DebugTools.AssertNotNull(_entity);
|
||||
|
||||
ClearOldStatus();
|
||||
|
||||
var collectMsg = new ItemStatusCollectMessage();
|
||||
_entityManager.EventBus.RaiseLocalEvent(_entity!.Value, collectMsg, true);
|
||||
|
||||
foreach (var control in collectMsg.Controls)
|
||||
{
|
||||
StatusContents.AddChild(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Storage;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Windows;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.Inventory.ClientInventorySystem;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory;
|
||||
|
||||
public sealed class InventoryUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>,
|
||||
IOnSystemChanged<ClientInventorySystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
[UISystemDependency] private readonly ClientInventorySystem _inventorySystem = default!;
|
||||
|
||||
private ClientInventoryComponent? _playerInventory;
|
||||
private readonly Dictionary<string, ItemSlotButtonContainer> _slotGroups = new();
|
||||
|
||||
private StrippingWindow? _strippingWindow;
|
||||
private ItemSlotButtonContainer? _inventoryHotbar;
|
||||
private MenuButton? _inventoryButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_strippingWindow == null);
|
||||
_strippingWindow = UIManager.CreateWindow<StrippingWindow>();
|
||||
_inventoryButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().InventoryButton;
|
||||
LayoutContainer.SetAnchorPreset(_strippingWindow, LayoutContainer.LayoutPreset.Center);
|
||||
|
||||
//bind open inventory key to OpenInventoryMenu;
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenInventoryMenu, InputCmdHandler.FromDelegate(_ => ToggleInventoryBar()))
|
||||
.Register<ClientInventorySystem>();
|
||||
_inventoryButton.OnPressed += InventoryButtonPressed;
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_strippingWindow != null)
|
||||
{
|
||||
_strippingWindow.Dispose();
|
||||
_strippingWindow = null;
|
||||
}
|
||||
|
||||
if (_inventoryHotbar != null)
|
||||
{
|
||||
_inventoryHotbar.Visible = false;
|
||||
}
|
||||
|
||||
if (_inventoryButton != null)
|
||||
{
|
||||
_inventoryButton.OnPressed -= InventoryButtonPressed;
|
||||
_inventoryButton.Pressed = false;
|
||||
_inventoryButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<ClientInventorySystem>();
|
||||
}
|
||||
|
||||
public void RegisterInventoryBarContainer(ItemSlotButtonContainer inventoryHotbar)
|
||||
{
|
||||
_inventoryHotbar = inventoryHotbar;
|
||||
}
|
||||
|
||||
private void InventoryButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleInventoryBar();
|
||||
}
|
||||
|
||||
private void UpdateInventoryHotbar(ClientInventoryComponent? clientInv)
|
||||
{
|
||||
if (clientInv == null)
|
||||
{
|
||||
_inventoryHotbar?.ClearButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (_, data) in clientInv.SlotData)
|
||||
{
|
||||
if (!data.ShowInWindow || !_slotGroups.TryGetValue(data.SlotGroup, out var container))
|
||||
continue;
|
||||
|
||||
if (!container.TryGetButton(data.SlotName, out var button))
|
||||
{
|
||||
button = new SlotButton(data);
|
||||
button.Pressed += ItemPressed;
|
||||
button.StoragePressed += StoragePressed;
|
||||
container.AddButton(button);
|
||||
}
|
||||
|
||||
var sprite = _entities.GetComponentOrNull<SpriteComponent>(data.HeldEntity);
|
||||
var showStorage = _entities.HasComponent<ClientStorageComponent>(data.HeldEntity);
|
||||
var update = new SlotSpriteUpdate(data.SlotGroup, data.SlotName, sprite, showStorage);
|
||||
SpriteUpdated(update);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStrippingWindow(ClientInventoryComponent? clientInv)
|
||||
{
|
||||
if (clientInv == null)
|
||||
{
|
||||
_strippingWindow!.InventoryButtons.ClearButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (_, data) in clientInv.SlotData)
|
||||
{
|
||||
if (!data.ShowInWindow)
|
||||
continue;
|
||||
|
||||
if (!_strippingWindow!.InventoryButtons.TryGetButton(data.SlotName, out var button))
|
||||
{
|
||||
button = new SlotButton(data);
|
||||
button.Pressed += ItemPressed;
|
||||
button.StoragePressed += StoragePressed;
|
||||
_strippingWindow!.InventoryButtons.AddButton(button, data.ButtonOffset);
|
||||
}
|
||||
|
||||
var sprite = _entities.GetComponentOrNull<SpriteComponent>(data.HeldEntity);
|
||||
var showStorage = _entities.HasComponent<ClientStorageComponent>(data.HeldEntity);
|
||||
var update = new SlotSpriteUpdate(data.SlotGroup, data.SlotName, sprite, showStorage);
|
||||
SpriteUpdated(update);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleStrippingMenu()
|
||||
{
|
||||
UpdateStrippingWindow(_playerInventory);
|
||||
if (_strippingWindow!.IsOpen)
|
||||
{
|
||||
_strippingWindow!.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_strippingWindow.Open();
|
||||
}
|
||||
|
||||
public void ToggleInventoryBar()
|
||||
{
|
||||
if (_inventoryHotbar == null)
|
||||
{
|
||||
Logger.Warning("Tried to toggle inventory bar when none are assigned");
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateInventoryHotbar(_playerInventory);
|
||||
if (_inventoryHotbar.Visible)
|
||||
{
|
||||
_inventoryHotbar.Visible = false;
|
||||
if (_inventoryButton != null)
|
||||
_inventoryButton.Pressed = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inventoryHotbar.Visible = true;
|
||||
if (_inventoryButton != null)
|
||||
_inventoryButton.Pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Neuron Activation
|
||||
public void OnSystemLoaded(ClientInventorySystem system)
|
||||
{
|
||||
_inventorySystem.OnSlotAdded += AddSlot;
|
||||
_inventorySystem.OnSlotRemoved += RemoveSlot;
|
||||
_inventorySystem.OnLinkInventory += LoadSlots;
|
||||
_inventorySystem.OnUnlinkInventory += UnloadSlots;
|
||||
_inventorySystem.OnSpriteUpdate += SpriteUpdated;
|
||||
}
|
||||
|
||||
// Neuron Deactivation
|
||||
public void OnSystemUnloaded(ClientInventorySystem system)
|
||||
{
|
||||
_inventorySystem.OnSlotAdded -= AddSlot;
|
||||
_inventorySystem.OnSlotRemoved -= RemoveSlot;
|
||||
_inventorySystem.OnLinkInventory -= LoadSlots;
|
||||
_inventorySystem.OnUnlinkInventory -= UnloadSlots;
|
||||
_inventorySystem.OnSpriteUpdate -= SpriteUpdated;
|
||||
}
|
||||
|
||||
private void ItemPressed(GUIBoundKeyEventArgs args, SlotControl control)
|
||||
{
|
||||
var slot = control.SlotName;
|
||||
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_inventorySystem.UIInventoryActivate(control.SlotName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
_inventorySystem.UIInventoryStorageActivate(control.SlotName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerInventory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_inventorySystem.UIInventoryExamine(slot, _playerInventory.Owner);
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UseSecondary)
|
||||
{
|
||||
_inventorySystem.UIInventoryOpenContextMenu(slot, _playerInventory.Owner);
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
_inventorySystem.UIInventoryActivateItem(slot, _playerInventory.Owner);
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_inventorySystem.UIInventoryAltActivateItem(slot, _playerInventory.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void StoragePressed(GUIBoundKeyEventArgs args, SlotControl control)
|
||||
{
|
||||
_inventorySystem.UIInventoryStorageActivate(control.SlotName);
|
||||
}
|
||||
|
||||
private void AddSlot(SlotData data)
|
||||
{
|
||||
if (!_slotGroups.TryGetValue(data.SlotGroup, out var slotGroup))
|
||||
return;
|
||||
|
||||
var button = new SlotButton(data);
|
||||
button.Pressed += ItemPressed;
|
||||
button.StoragePressed += StoragePressed;
|
||||
slotGroup.AddButton(button);
|
||||
}
|
||||
|
||||
private void RemoveSlot(SlotData data)
|
||||
{
|
||||
if (!_slotGroups.TryGetValue(data.SlotGroup, out var slotGroup))
|
||||
return;
|
||||
|
||||
slotGroup.RemoveButton(data.SlotName);
|
||||
}
|
||||
|
||||
private void LoadSlots(ClientInventoryComponent clientInv)
|
||||
{
|
||||
UnloadSlots();
|
||||
_playerInventory = clientInv;
|
||||
foreach (var slotData in clientInv.SlotData.Values)
|
||||
{
|
||||
AddSlot(slotData);
|
||||
}
|
||||
|
||||
UpdateInventoryHotbar(_playerInventory);
|
||||
}
|
||||
|
||||
private void UnloadSlots()
|
||||
{
|
||||
_playerInventory = null;
|
||||
foreach (var slotGroup in _slotGroups.Values)
|
||||
{
|
||||
slotGroup.ClearButtons();
|
||||
}
|
||||
}
|
||||
|
||||
private void SpriteUpdated(SlotSpriteUpdate update)
|
||||
{
|
||||
var (group, name, sprite, showStorage) = update;
|
||||
|
||||
if (_strippingWindow?.InventoryButtons.GetButton(update.Name) is { } inventoryButton)
|
||||
{
|
||||
inventoryButton.SpriteView.Sprite = sprite;
|
||||
inventoryButton.StorageButton.Visible = showStorage;
|
||||
}
|
||||
|
||||
if (_slotGroups.GetValueOrDefault(group)?.GetButton(name) is not { } button)
|
||||
return;
|
||||
|
||||
button.SpriteView.Sprite = sprite;
|
||||
button.StorageButton.Visible = showStorage;
|
||||
}
|
||||
|
||||
public bool RegisterSlotGroupContainer(ItemSlotButtonContainer slotContainer)
|
||||
{
|
||||
if (_slotGroups.TryAdd(slotContainer.SlotGroup, slotContainer))
|
||||
return true;
|
||||
|
||||
Logger.Warning("Could not add container for slotgroup: " + slotContainer.SlotGroup);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RemoveSlotGroup(string slotGroupName)
|
||||
{
|
||||
_slotGroups.Remove(slotGroupName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<windows:StrippingWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Windows"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Controls"
|
||||
Name="StrippingDisplay"
|
||||
HorizontalExpand="True"
|
||||
Title="Stripping"
|
||||
VerticalExpand="True"
|
||||
>
|
||||
<controls:InventoryDisplay Name="InventoryButtons" Access="Public"/>
|
||||
</windows:StrippingWindow>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Inventory.Windows;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StrippingWindow : UserInterface.Controls.FancyWindow
|
||||
{
|
||||
public StrippingWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.Center);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<widgets:GameTopMenuBar xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:ic="clr-namespace:Robust.Shared.Input;assembly=Robust.Shared"
|
||||
xmlns:is="clr-namespace:Content.Shared.Input;assembly=Content.Shared"
|
||||
xmlns:xe="clr-namespace:Content.Client.UserInterface.XamlExtensions"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
|
||||
Name = "MenuButtons"
|
||||
VerticalExpand="False"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
SeparationOverride="5"
|
||||
>
|
||||
<ui:MenuButton
|
||||
Name="EscapeButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/hamburger.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static ic:EngineKeyFunctions.EscapeMenu}"
|
||||
ToolTip="{Loc 'game-hud-open-escape-menu-button-tooltip'}"
|
||||
MinSize="70 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="CharacterButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/character.svg.192dpi.png'}"
|
||||
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="InventoryButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/inventory.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenInventoryMenu}"
|
||||
ToolTip="{Loc 'game-hud-open-inventory-menu-button-tooltip'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="CraftingButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/hammer.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
|
||||
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="ActionButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
|
||||
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="AdminButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/gavel.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
|
||||
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="SandboxButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/sandbox.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
|
||||
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
|
||||
/>
|
||||
<ui:MenuButton
|
||||
Name="AHelpButton"
|
||||
Access="Internal"
|
||||
Icon="{xe:Tex '/Textures/Interface/info.svg.192dpi.png'}"
|
||||
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
|
||||
ToolTip="{Loc 'ui-options-function-open-ahelp'}"
|
||||
MinSize="42 64"
|
||||
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
|
||||
/>
|
||||
</widgets:GameTopMenuBar>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.MenuBar.Widgets
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GameTopMenuBar : UIWidget
|
||||
{
|
||||
public GameTopMenuBar()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<controls:ObjectiveBriefingControl
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Objectives.Controls"
|
||||
Orientation="Horizontal">
|
||||
<Label Name="Label" Access="Public" Modulate="#FFFF00"/>
|
||||
</controls:ObjectiveBriefingControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Objectives.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ObjectiveBriefingControl : BoxContainer
|
||||
{
|
||||
public ObjectiveBriefingControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<controls:ObjectiveConditionsControl
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Objectives.Controls"
|
||||
Orientation="Horizontal">
|
||||
<cc:ProgressTextureRect Name="ProgressTexture" VerticalAlignment="Center" Access="Public"/>
|
||||
<Control MinSize="10 0"/>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="Title" Access="Public"/>
|
||||
<Label Name="Description" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</controls:ObjectiveConditionsControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Objectives.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ObjectiveConditionsControl : BoxContainer
|
||||
{
|
||||
public ObjectiveConditionsControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Markers;
|
||||
using Content.Client.Sandbox;
|
||||
using Content.Client.SubFloor;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.DecalPlacer;
|
||||
using Content.Client.UserInterface.Systems.Sandbox.Windows;
|
||||
using Content.Shared.Input;
|
||||
@@ -13,11 +13,11 @@ using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controllers.Implementations;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Sandbox;
|
||||
|
||||
@@ -28,7 +28,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
[Dependency] private readonly IEyeManager _eye = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
[Dependency] private readonly ILightManager _light = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
[UISystemDependency] private readonly DebugPhysicsSystem _debugPhysics = default!;
|
||||
[UISystemDependency] private readonly MarkerSystem _marker = default!;
|
||||
@@ -42,11 +41,14 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
private TileSpawningUIController TileSpawningController => UIManager.GetUIController<TileSpawningUIController>();
|
||||
private DecalPlacerUIController DecalPlacerController => UIManager.GetUIController<DecalPlacerUIController>();
|
||||
|
||||
private MenuButton? _sandboxButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_sandboxButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().SandboxButton;
|
||||
_sandboxButton.OnPressed += SandboxButtonPressed;
|
||||
EnsureWindow();
|
||||
_gameHud.SandboxButtonToggled += GameHudOnSandboxButtonToggled;
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenEntitySpawnWindow,
|
||||
InputCmdHandler.FromDelegate(_ => EntitySpawningController.ToggleWindow()));
|
||||
@@ -67,8 +69,8 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
if(_window is { Disposed: false })
|
||||
return;
|
||||
_window = UIManager.CreateWindow<SandboxWindow>();
|
||||
_window.OnClose += () => { _gameHud.SandboxButtonDown = false; };
|
||||
_window.OnOpen += () => { _gameHud.SandboxButtonDown = true; };
|
||||
_window.OnOpen += () => { _sandboxButton!.Pressed = true; };
|
||||
_window.OnClose += () => { _sandboxButton!.Pressed = false; };
|
||||
_window.ToggleLightButton.Pressed = !_light.Enabled;
|
||||
_window.ToggleFovButton.Pressed = !_eye.CurrentEye.DrawFov;
|
||||
_window.ToggleShadowsButton.Pressed = !_light.DrawShadows;
|
||||
@@ -105,8 +107,12 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
_window = null;
|
||||
}
|
||||
|
||||
_gameHud.SandboxButtonToggled -= GameHudOnSandboxButtonToggled;
|
||||
_gameHud.SandboxButtonDown = false;
|
||||
if (_sandboxButton != null)
|
||||
{
|
||||
_sandboxButton.Pressed = false;
|
||||
_sandboxButton.OnPressed -= SandboxButtonPressed;
|
||||
_sandboxButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<SandboxSystem>();
|
||||
}
|
||||
@@ -121,6 +127,11 @@ public sealed class SandboxUIController : UIController, IOnStateChanged<Gameplay
|
||||
system.SandboxDisabled -= CloseAll;
|
||||
}
|
||||
|
||||
private void SandboxButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void CloseAll()
|
||||
{
|
||||
_window?.Close();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Client.Resources;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.ResourceManagement;
|
||||
|
||||
namespace Content.Client.UserInterface.XamlExtensions;
|
||||
|
||||
|
||||
[PublicAPI]
|
||||
public sealed class TexExtension
|
||||
{
|
||||
private IResourceCache _resourceCache;
|
||||
public string Path { get; }
|
||||
|
||||
public TexExtension(string path)
|
||||
{
|
||||
_resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public object ProvideValue()
|
||||
{
|
||||
return _resourceCache.GetTexture(Path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user