Files
tbd-station-14/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
pathetic meowmeow 8e24308714 Make two inventories not dance around as much when opening/closing them (#35041)
* Make two inventories not dance around as much when opening/closing them

* Use .Any
2025-04-19 10:40:49 -04:00

433 lines
16 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Client.Examine;
using Content.Client.Hands.Systems;
using Content.Client.Interaction;
using Content.Client.Storage;
using Content.Client.Storage.Systems;
using Content.Client.UserInterface.Systems.Hotbar.Widgets;
using Content.Client.UserInterface.Systems.Info;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Client.Verbs.UI;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Input;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface.Systems.Storage;
public sealed class StorageUIController : UIController, IOnSystemChanged<StorageSystem>
{
/*
* Things are a bit over the shop but essentially
* - Clicking into storagewindow is handled via storagewindow
* - Clicking out of it is via ItemGridPiece
* - Dragging around is handled here
* - Drawing is handled via ItemGridPiece
* - StorageSystem handles any sim stuff around open windows.
*/
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly CloseRecentWindowUIController _closeRecentWindowUIController = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
[UISystemDependency] private readonly UserInterfaceSystem _ui = default!;
private readonly DragDropHelper<ItemGridPiece> _menuDragHelper;
public ItemGridPiece? DraggingGhost => _menuDragHelper.Dragged;
public Angle DraggingRotation = Angle.Zero;
public bool StaticStorageUIEnabled;
public bool OpaqueStorageWindow;
private int _openStorageLimit = -1;
public bool IsDragging => _menuDragHelper.IsDragging;
public ItemGridPiece? CurrentlyDragging => _menuDragHelper.Dragged;
public bool WindowTitle { get; private set; } = false;
public StorageUIController()
{
_menuDragHelper = new DragDropHelper<ItemGridPiece>(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag);
}
public override void Initialize()
{
base.Initialize();
_configuration.OnValueChanged(CCVars.StaticStorageUI, OnStaticStorageChanged, true);
_configuration.OnValueChanged(CCVars.OpaqueStorageWindow, OnOpaqueWindowChanged, true);
_configuration.OnValueChanged(CCVars.StorageWindowTitle, OnStorageWindowTitle, true);
_configuration.OnValueChanged(CCVars.StorageLimit, OnStorageLimitChanged, true);
}
private void OnStorageLimitChanged(int obj)
{
_openStorageLimit = obj;
}
private void OnStorageWindowTitle(bool obj)
{
WindowTitle = obj;
}
private void OnOpaqueWindowChanged(bool obj)
{
OpaqueStorageWindow = obj;
}
private void OnStaticStorageChanged(bool obj)
{
StaticStorageUIEnabled = obj;
}
public StorageWindow CreateStorageWindow(StorageBoundUserInterface sBui)
{
var window = new StorageWindow();
window.MouseFilter = Control.MouseFilterMode.Pass;
window.OnPiecePressed += (args, piece) =>
{
OnPiecePressed(args, window, piece);
};
window.OnPieceUnpressed += (args, piece) =>
{
OnPieceUnpressed(args, window, piece);
};
if (StaticStorageUIEnabled)
{
var hotbar = UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
// this lambda handles the nested storage case
// during nested storage, a parent window hides and a child window is
// immediately inserted to the end of the list
// we can reorder the newly inserted to the same index as the invisible
// window in order to prevent an invisible window from being replaced
// with a visible one in a different position
Action<Control?, Control> reorder = (parent, child) =>
{
if (parent is null)
return;
var parentChildren = parent.Children.ToList();
var invisibleIndex = parentChildren.FindIndex(c => c.Visible == false);
if (invisibleIndex == -1)
return;
child.SetPositionInParent(invisibleIndex);
};
if (_openStorageLimit == 2)
{
if (hotbar?.LeftStorageContainer.Children.Any(c => c.Visible) == false) // we're comparing booleans because it's bool? and not bool from the optional chaining
{
hotbar?.LeftStorageContainer.AddChild(window);
reorder(hotbar?.LeftStorageContainer, window);
}
else
{
hotbar?.RightStorageContainer.AddChild(window);
reorder(hotbar?.RightStorageContainer, window);
}
}
else
{
hotbar?.SingleStorageContainer.AddChild(window);
reorder(hotbar?.SingleStorageContainer, window);
}
_closeRecentWindowUIController.SetMostRecentlyInteractedWindow(window);
}
else
{
// Open at parent position if it's open.
if (_ui.TryGetOpenUi<StorageBoundUserInterface>(EntityManager.GetComponent<TransformComponent>(sBui.Owner).ParentUid,
StorageComponent.StorageUiKey.Key, out var bui) && bui.Position != null)
{
window.Open(bui.Position.Value);
}
// Open at the saved position if it exists.
else if (_ui.TryGetPosition(sBui.Owner, StorageComponent.StorageUiKey.Key, out var pos))
{
window.Open(pos);
}
// Open at the default position.
else
{
window.OpenCenteredLeft();
}
}
_ui.RegisterControl(sBui, window);
return window;
}
public void OnSystemLoaded(StorageSystem system)
{
_input.FirstChanceOnKeyEvent += OnMiddleMouse;
}
public void OnSystemUnloaded(StorageSystem system)
{
_input.FirstChanceOnKeyEvent -= OnMiddleMouse;
}
/// One might ask, Hey Emo, why are you parsing raw keyboard input just to rotate a rectangle?
/// The answer is, that input bindings regarding mouse inputs are always intercepted by the UI,
/// thus, if i want to be able to rotate my damn piece anywhere on the screen,
/// I have to side-step all of the input handling. Cheers.
private void OnMiddleMouse(KeyEventArgs keyEvent, KeyEventType type)
{
if (keyEvent.Handled)
return;
if (type != KeyEventType.Down)
return;
//todo there's gotta be a method for this in InputManager just expose it to content I BEG.
if (!_input.TryGetKeyBinding(ContentKeyFunctions.RotateStoredItem, out var binding))
return;
if (binding.BaseKey != keyEvent.Key)
return;
if (keyEvent.Shift &&
!(binding.Mod1 == Keyboard.Key.Shift ||
binding.Mod2 == Keyboard.Key.Shift ||
binding.Mod3 == Keyboard.Key.Shift))
return;
if (keyEvent.Alt &&
!(binding.Mod1 == Keyboard.Key.Alt ||
binding.Mod2 == Keyboard.Key.Alt ||
binding.Mod3 == Keyboard.Key.Alt))
return;
if (keyEvent.Control &&
!(binding.Mod1 == Keyboard.Key.Control ||
binding.Mod2 == Keyboard.Key.Control ||
binding.Mod3 == Keyboard.Key.Control))
return;
if (!IsDragging && EntityManager.System<HandsSystem>().GetActiveHandEntity() == null)
return;
//clamp it to a cardinal.
DraggingRotation = (DraggingRotation + Math.PI / 2f).GetCardinalDir().ToAngle();
if (DraggingGhost != null)
DraggingGhost.Location.Rotation = DraggingRotation;
if (IsDragging || UIManager.CurrentlyHovered is StorageWindow)
keyEvent.Handle();
}
private void OnPiecePressed(GUIBoundKeyEventArgs args, StorageWindow window, ItemGridPiece control)
{
if (IsDragging || !window.IsOpen)
return;
if (args.Function == ContentKeyFunctions.MoveStoredItem)
{
DraggingRotation = control.Location.Rotation;
_menuDragHelper.MouseDown(control);
_menuDragHelper.Update(0f);
args.Handle();
}
else if (args.Function == ContentKeyFunctions.SaveItemLocation)
{
if (window.StorageEntity is not {} storage)
return;
EntityManager.RaisePredictiveEvent(new StorageSaveItemLocationEvent(
EntityManager.GetNetEntity(control.Entity),
EntityManager.GetNetEntity(storage)));
args.Handle();
}
else if (args.Function == ContentKeyFunctions.ExamineEntity)
{
EntityManager.System<ExamineSystem>().DoExamine(control.Entity);
args.Handle();
}
else if (args.Function == EngineKeyFunctions.UseSecondary)
{
UIManager.GetUIController<VerbMenuUIController>().OpenVerbMenu(control.Entity);
args.Handle();
}
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
{
EntityManager.RaisePredictiveEvent(
new InteractInventorySlotEvent(EntityManager.GetNetEntity(control.Entity), altInteract: false));
args.Handle();
}
else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(EntityManager.GetNetEntity(control.Entity), altInteract: true));
args.Handle();
}
window.FlagDirty();
}
private void OnPieceUnpressed(GUIBoundKeyEventArgs args, StorageWindow window, ItemGridPiece control)
{
if (args.Function != ContentKeyFunctions.MoveStoredItem)
return;
// Want to get the control under the dragged control.
// This means we can drag the original control around (and not hide the original).
control.MouseFilter = Control.MouseFilterMode.Ignore;
var targetControl = UIManager.MouseGetControl(args.PointerLocation);
var targetStorage = targetControl as StorageWindow;
control.MouseFilter = Control.MouseFilterMode.Pass;
var localPlayer = _player.LocalEntity;
window.RemoveGrid(control);
window.FlagDirty();
// If we tried to drag it on top of another grid piece then cancel out.
if (targetControl is ItemGridPiece || window.StorageEntity is not { } sourceStorage || localPlayer == null)
{
window.Reclaim(control.Location, control);
args.Handle();
_menuDragHelper.EndDrag();
return;
}
if (_menuDragHelper.IsDragging && DraggingGhost is { } draggingGhost)
{
var dragEnt = draggingGhost.Entity;
var dragLoc = draggingGhost.Location;
// Dragging in the same storage
// The existing ItemGridPiece just stops rendering but still exists so check if it's hovered.
if (targetStorage == window)
{
var position = targetStorage.GetMouseGridPieceLocation(dragEnt, dragLoc);
var newLocation = new ItemStorageLocation(DraggingRotation, position);
if (!_storage.ItemFitsInGridLocation(dragEnt, sourceStorage, newLocation))
{
window.Reclaim(control.Location, control);
}
else
{
EntityManager.RaisePredictiveEvent(new StorageSetItemLocationEvent(
EntityManager.GetNetEntity(draggingGhost.Entity),
EntityManager.GetNetEntity(sourceStorage),
newLocation));
window.Reclaim(newLocation, control);
}
}
// Dragging to new storage
else if (targetStorage?.StorageEntity != null && targetStorage != window)
{
var position = targetStorage.GetMouseGridPieceLocation(dragEnt, dragLoc);
var newLocation = new ItemStorageLocation(DraggingRotation, position);
// Check it fits and we can move to hand (no free transfers).
if (_storage.ItemFitsInGridLocation(
(dragEnt, null),
(targetStorage.StorageEntity.Value, null),
newLocation))
{
// Can drop and move.
EntityManager.RaisePredictiveEvent(new StorageTransferItemEvent(
EntityManager.GetNetEntity(dragEnt),
EntityManager.GetNetEntity(targetStorage.StorageEntity.Value),
newLocation));
targetStorage.Reclaim(newLocation, control);
DraggingRotation = Angle.Zero;
}
else
{
// Cancel it (rather than dropping).
window.Reclaim(dragLoc, control);
}
}
targetStorage?.FlagDirty();
}
// If we just clicked, then take it out of the bag.
else
{
EntityManager.RaisePredictiveEvent(new StorageInteractWithItemEvent(
EntityManager.GetNetEntity(control.Entity),
EntityManager.GetNetEntity(sourceStorage)));
}
_menuDragHelper.EndDrag();
args.Handle();
}
private bool OnMenuBeginDrag()
{
if (_menuDragHelper.Dragged is not { } dragged)
return false;
DraggingGhost!.Orphan();
DraggingRotation = dragged.Location.Rotation;
UIManager.PopupRoot.AddChild(DraggingGhost);
SetDraggingRotation();
return true;
}
private bool OnMenuContinueDrag(float frameTime)
{
if (DraggingGhost == null)
return false;
var player = _player.LocalEntity;
// If the attached storage is closed then stop dragging
if (player == null ||
!_storage.TryGetStorageLocation(DraggingGhost.Entity, out var container, out _, out _) ||
!_ui.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, player.Value))
{
DraggingGhost.Orphan();
return false;
}
SetDraggingRotation();
return true;
}
private void SetDraggingRotation()
{
if (DraggingGhost == null)
return;
var offset = ItemGridPiece.GetCenterOffset(
(DraggingGhost.Entity, null),
new ItemStorageLocation(DraggingRotation, Vector2i.Zero),
EntityManager);
// I don't know why it divides the position by 2. Hope this helps! -emo
LayoutContainer.SetPosition(DraggingGhost, UIManager.MousePositionScaled.Position / 2 - offset );
}
private void OnMenuEndDrag()
{
if (DraggingGhost == null)
return;
DraggingRotation = Angle.Zero;
}
public override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_menuDragHelper.Update(args.DeltaSeconds);
}
}