Skip to content

Commit

Permalink
ChemMaster+ (#1585)
Browse files Browse the repository at this point in the history
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Changes how the ChemMaster works:
1. Removes the amount buttons and instead uses a textbox that resets
whenever a value is entered or focus ends.
2. Shrinks the ChemMaster again.
3. Adds sorting options, including for quantity, last added, and a sort
option for the (default) alphabetical order.
4. Sorting options save via the ChemMaster itself, not per-user.

Video showcase:
https://discord.com/channels/1218698320155906090/1218698321053356060/1330129166384894046

---

<!--
You can add an author after the `:cl:` to change the name that appears
in the changelog (ex: `:cl: Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

:cl:
- add: Added sorting options to the ChemMaster.
- add: Added the ability to input custom amounts into the ChemMaster via
a textbox that resets on change.
- tweak: The width of the ChemMaster UI has been lowered.
- remove: Removed quantity buttons from the ChemMaster.

---------

Co-authored-by: VMSolidus <[email protected]>
  • Loading branch information
sleepyyapril and VMSolidus committed Jan 20, 2025
1 parent 71eb7fe commit 7b5b033
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 60 deletions.
13 changes: 6 additions & 7 deletions Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@ namespace Content.Client.Chemistry.UI
/// Initializes a <see cref="ChemMasterWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class ChemMasterBoundUserInterface : BoundUserInterface
public sealed class ChemMasterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private ChemMasterWindow? _window;

public ChemMasterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}

/// <summary>
/// Called each time a chem master UI instance is opened. Generates the window and fills it with
/// relevant info. Sets the actions for static buttons.
Expand Down Expand Up @@ -49,7 +45,11 @@ protected override void Open()
_window.PillTypeButtons[i].OnPressed += _ => SendMessage(new ChemMasterSetPillTypeMessage(pillType));
}

_window.OnReagentButtonPressed += (args, button) => SendMessage(new ChemMasterReagentAmountButtonMessage(button.Id, button.Amount, button.IsBuffer));
_window.OnReagentButtonPressed += (_, button, amount) => SendMessage(new ChemMasterReagentAmountButtonMessage(button.Id, amount, button.IsBuffer));
_window.OnSortMethodChanged += sortMethod => SendMessage(new ChemMasterSortMethodUpdated(sortMethod));
_window.OnTransferAmountChanged += amount => SendMessage(new ChemMasterTransferringAmountUpdated(amount));


}

/// <summary>
Expand All @@ -64,7 +64,6 @@ protected override void UpdateState(BoundUserInterfaceState state)
base.UpdateState(state);

var castState = (ChemMasterBoundUserInterfaceState) state;

_window?.UpdateState(castState); // Update window state
}
}
Expand Down
16 changes: 13 additions & 3 deletions Content.Client/Chemistry/UI/ChemMasterWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="620 670"
MinSize="500 770"
Title="{Loc 'chem-master-bound-user-interface-title'}">
<TabContainer Name="Tabs" Margin="5 5 7 5">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="5" SeparationOverride="10">
<!-- Input container info -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-container-label'}" />
<Control HorizontalExpand="True" />
<Button MinSize="80 0" Name="InputEjectButton" Access="Public" Text="{Loc 'chem-master-window-eject-button'}" />
<RichTextLabel Name="InputAmountLabel" Text="{Loc 'chem-master-window-transferring-default-label'}" Margin="0 0 7 0"/>
<LineEdit MinSize="140 0" Name="InputAmountLineEdit" Access="Public" PlaceHolder="{Loc 'chem-master-window-amount-placeholder'}" />
<Button MinSize="80 0" Name="InputEjectButton" Access="Public" Text="{Loc 'chem-master-window-eject-button'}" StyleClasses="OpenLeft" />
</BoxContainer>

<PanelContainer VerticalExpand="True" MinSize="0 200">
Expand All @@ -30,7 +32,13 @@
<Control MinSize="0 10" />

<!-- Buffer -->
<Label Text="{Loc 'chem-master-window-buffer-text'}" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-buffer-text'}" />
<Control HorizontalExpand="True" />
<Button MinSize="80 0" Name="BufferTransferButton" Access="Public" Text="{Loc 'chem-master-window-transfer-button'}" ToggleMode="True" StyleClasses="OpenRight" />
<OptionButton Name="SortMethod" Access="Public" ToolTip="{Loc 'chem-master-window-sort-method-tooltip'}" StyleClasses="OpenBoth" />
<Button MinSize="80 0" Name="BufferDiscardButton" Access="Public" Text="{Loc 'chem-master-window-discard-button'}" ToggleMode="True" StyleClasses="OpenLeft" />
</BoxContainer>

<!-- Buffer info -->
<PanelContainer VerticalExpand="True" MinSize="0 200">
Expand All @@ -52,6 +60,8 @@
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-container-label'}" />
<Control HorizontalExpand="True" />
<Label Name="OutputAmountLabel" Text="{Loc 'chem-master-window-transferring-default-label'}" Margin="0 0 7 0"/>
<LineEdit MinSize="140 0" Name="OutputAmountLineEdit" Access="Public" PlaceHolder="{Loc 'chem-master-window-amount-placeholder'}" />
<Button MinSize="80 0" Name="OutputEjectButton" Access="Public" Text="{Loc 'chem-master-window-eject-button'}" />
</BoxContainer>

Expand Down
224 changes: 186 additions & 38 deletions Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,19 @@ namespace Content.Client.Chemistry.UI
public sealed partial class ChemMasterWindow : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action<BaseButton.ButtonEventArgs, ReagentButton>? OnReagentButtonPressed;
public event Action<BaseButton.ButtonEventArgs, ReagentButton, int>? OnReagentButtonPressed;
public event Action<int>? OnSortMethodChanged;
public event Action<int>? OnTransferAmountChanged;
public readonly Button[] PillTypeButtons;

private Dictionary<string, ReagentCached> _reagents;
private const string TransferringAmountColor = "#ffa500";
private ReagentSortMethod _currentSortMethod = ReagentSortMethod.Alphabetical;
private ChemMasterBoundUserInterfaceState? _lastState;
private string _lastAmountText = "50";
private int _transferAmount = 50;


private const string PillsRsiPath = "/Textures/Objects/Specific/Chemistry/pills.rsi";

/// <summary>
Expand All @@ -38,6 +48,14 @@ public ChemMasterWindow()
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

_reagents = new();

InputAmountLineEdit.OnTextEntered += SetAmount;
InputAmountLineEdit.OnFocusExit += SetAmount;

OutputAmountLineEdit.OnTextEntered += SetAmount;
OutputAmountLineEdit.OnFocusExit += SetAmount;

// Pill type selection buttons, in total there are 20 pills.
// Pill rsi file should have states named as pill1, pill2, and so on.
var resourcePath = new ResPath(PillsRsiPath);
Expand Down Expand Up @@ -87,44 +105,118 @@ public ChemMasterWindow()

Tabs.SetTabTitle(0, Loc.GetString("chem-master-window-input-tab"));
Tabs.SetTabTitle(1, Loc.GetString("chem-master-window-output-tab"));

SortMethod.AddItem(
Loc.GetString("chem-master-window-sort-method-Alphabetical-text"),
(int) ReagentSortMethod.Alphabetical);
SortMethod.AddItem(Loc.GetString("chem-master-window-sort-method-Amount-text"), (int) ReagentSortMethod.Amount);
SortMethod.AddItem(Loc.GetString("chem-master-window-sort-method-Time-text"), (int) ReagentSortMethod.Time);
SortMethod.OnItemSelected += HandleChildPressed;

BufferTransferButton.OnPressed += HandleDiscardTransferPress;
BufferDiscardButton.OnPressed += HandleDiscardTransferPress;
}

private void HandleDiscardTransferPress(BaseButton.ButtonEventArgs args)
{
var buttons = BufferInfo.Children
.Where(c => c is Button)
.Cast<Button>();

foreach (var button in buttons)
{
var text = BufferTransferButton.Pressed ? "transfer" : "discard";
button.Text = Loc.GetString($"chem-master-window-{text}-button-text");
}
}

private void HandleSortMethodChange(int newSortMethod)
{
if (newSortMethod == (int) _currentSortMethod)
return;

_currentSortMethod = (ReagentSortMethod) newSortMethod;
SortMethod.SelectId(newSortMethod);
SortUpdated();
}

private void HandleChildPressed(OptionButton.ItemSelectedEventArgs args)
{
HandleSortMethodChange(args.Id);
OnSortMethodChanged?.Invoke(args.Id);
}

private ReagentButton MakeReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
private void SortUpdated()
{
var reagentTransferButton = new ReagentButton(text, amount, id, isBuffer, styleClass);
if (_lastState == null)
return;

UpdatePanelInfo(_lastState);
}

private bool ValidateAmount(string newText)
{
if (string.IsNullOrWhiteSpace(newText) || !int.TryParse(newText, out int amount))
{
InputAmountLineEdit.SetText(string.Empty);
OutputAmountLineEdit.SetText(string.Empty);
return false;
}

_lastAmountText = newText;
_transferAmount = amount;
OnTransferAmountChanged?.Invoke(amount);
return true;
}

private void SetAmount(LineEdit.LineEditEventArgs args) =>
SetAmountText(args.Text);

private void SetAmountText(string newText)
{
if (newText == _lastAmountText)
return;

if (!ValidateAmount(newText))
return;

var localizedAmount = Loc.GetString(
"chem-master-window-transferring-label",
("quantity", newText),
("color", TransferringAmountColor));

InputAmountLabel.Text = localizedAmount;
OutputAmountLabel.Text = localizedAmount;

InputAmountLineEdit.SetText(string.Empty);
OutputAmountLineEdit.SetText(string.Empty);
}

private ReagentButton MakeReagentButton(string text, ReagentId id, bool isBuffer)
{
var reagentTransferButton = new ReagentButton(text, id, isBuffer);
reagentTransferButton.OnPressed += args
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton);
=> OnReagentButtonPressed?.Invoke(args, reagentTransferButton, _transferAmount);
return reagentTransferButton;
}
/// <summary>
/// Conditionally generates a set of reagent buttons based on the supplied boolean argument.
/// This was moved outside of BuildReagentRow to facilitate conditional logic, stops indentation depth getting out of hand as well.
/// </summary>
private List<ReagentButton> CreateReagentTransferButtons(ReagentId reagent, bool isBuffer, bool addReagentButtons)
private ReagentButton? CreateReagentTransferButton(ReagentId reagent, bool isBuffer, bool addReagentButtons)
{
if (!addReagentButtons)
return new List<ReagentButton>(); // Return an empty list if reagentTransferButton creation is disabled.
return null; // Return an empty list if reagentTransferButton creation is disabled.

var buttonConfigs = new (string text, ChemMasterReagentAmount amount, string styleClass)[]
{
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),
};
var text = BufferTransferButton.Pressed ? "transfer" : "discard";

var buttons = new List<ReagentButton>();

foreach (var (text, amount, styleClass) in buttonConfigs)
{
var reagentTransferButton = MakeReagentButton(text, amount, reagent, isBuffer, styleClass);
buttons.Add(reagentTransferButton);
}
var reagentTransferButton = MakeReagentButton(
Loc.GetString($"chem-master-window-{text}-button"),
reagent,
isBuffer
);

return buttons;
return reagentTransferButton;
}

/// <summary>
Expand All @@ -138,9 +230,13 @@ public void UpdateState(BoundUserInterfaceState state)
if (castState.UpdateLabel)
LabelLine = GenerateLabel(castState);

_lastState = castState;

// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);

HandleSortMethodChange(castState.SortMethod);
SetAmountText(castState.TransferringAmount.ToString());

BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";

InputEjectButton.Disabled = castState.InputContainerInfo is null;
Expand Down Expand Up @@ -233,15 +329,57 @@ private void UpdatePanelInfo(ChemMasterBoundUserInterfaceState state)
bufferHBox.AddChild(bufferVol);

// initialises rowCount to allow for striped rows

var rowCount = 0;
foreach (var (reagent, quantity) in state.BufferReagents)
var bufferReagents = state.BufferReagents.OrderBy(x => x.Reagent.Prototype);

if (_currentSortMethod == ReagentSortMethod.Amount)
bufferReagents = bufferReagents.OrderByDescending(x => x.Quantity);

if (_currentSortMethod == ReagentSortMethod.Time)
{
bufferReagents = bufferReagents.OrderByDescending(
x =>
{
var exists = _reagents.TryGetValue(x.Reagent.Prototype, out var reagent);
return exists && reagent != null ? reagent.TimeAdded : DateTimeOffset.UtcNow;
});
}

var bufferAsNames = bufferReagents.Select(r => r.Reagent.Prototype).ToHashSet();
var hashSetCachedReagents = _reagents.Keys.ToHashSet();
hashSetCachedReagents.ExceptWith(bufferAsNames);

foreach (var missing in hashSetCachedReagents)
_reagents.Remove(missing);

foreach (var (reagent, quantity) in bufferReagents)
{
var reagentId = reagent;
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
var reagentColor = proto?.SubstanceColor ?? default(Color);
BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));

var exists = _reagents.TryGetValue(reagent.Prototype, out var reagentCached);

if (!exists)
{
reagentCached = new()
{
Id = reagentId,
Quantity = quantity,
TimeAdded = reagentCached?.TimeAdded ?? DateTimeOffset.UtcNow
};

_reagents.Add(reagentId.Prototype, reagentCached);
}
else
{
reagentCached!.Quantity = quantity;
reagentCached!.Id = reagentId;

_reagents[reagentId.Prototype] = reagentCached;
}
}
}

Expand Down Expand Up @@ -311,8 +449,8 @@ private Control BuildReagentRow(Color reagentColor, int rowCount, string name, R
reagentColor = currentRowColor;
}
//this calls the separated button builder, and stores the return to render after labels
var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
var reagentButtonConstructor = CreateReagentTransferButton(reagent, isBuffer, addReagentButtons);

// Create the row layout with the color panel
var rowContainer = new BoxContainer
{
Expand Down Expand Up @@ -343,11 +481,9 @@ private Control BuildReagentRow(Color reagentColor, int rowCount, string name, R
}
};

// Add the reagent buttons after the color panel
foreach (var reagentTransferButton in reagentButtonConstructors)
{
rowContainer.AddChild(reagentTransferButton);
}
if (reagentButtonConstructor != null)
rowContainer.AddChild(reagentButtonConstructor);

//Apply panencontainer to allow for striped rows
return new PanelContainer
{
Expand All @@ -365,16 +501,28 @@ public string LabelLine

public sealed class ReagentButton : Button
{
public ChemMasterReagentAmount Amount { get; set; }
public bool IsBuffer = true;
public ReagentId Id { get; set; }
public ReagentButton(string text, ChemMasterReagentAmount amount, ReagentId id, bool isBuffer, string styleClass)
public ReagentButton(string text, ReagentId id, bool isBuffer)
{
AddStyleClass(styleClass);
AddStyleClass(StyleBase.ButtonOpenLeft);
Text = text;
Amount = amount;
Id = id;
IsBuffer = isBuffer;
}
}

public sealed class ReagentCached
{
public ReagentId Id { get; set; }
public DateTimeOffset TimeAdded { get; set; }
public FixedPoint2 Quantity { get; set; }
}

public enum ReagentSortMethod
{
Time,
Alphabetical,
Amount
}
}
Loading

0 comments on commit 7b5b033

Please sign in to comment.