Construction menu recipe history (#8)

This commit is contained in:
Vladislav Suchkov 2026-06-23 01:04:30 +10:00 • committed by GitHub
parent 48201fc0b0
commit 4dd1941c54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 545 additions and 187 deletions

View file

@ -2,13 +2,36 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'construction-menu-search'}" HorizontalExpand="True"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<controls:ListContainer Name="Recipes" Access="Public" Group="True" Toggle="True" VerticalExpand="True" />
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
<BoxContainer SizeFlagsStretchRatio="0.5" Margin="0 3 0 5" SeparationOverride="3">
<Button
Name="PreviousRecipeInHistoryButton"
HorizontalExpand="True"
Disabled="True"
StyleClasses="OpenRight">
<Label
StyleIdentifier="RecipeHistoryNavButtonLabel"
Align="Center"
Text="{Loc construction-menu-history-select-previous-recipe}"
/>
</Button>
<Button
Name="NextRecipeInHistoryButton"
HorizontalExpand="True"
Disabled="True"
StyleClasses="OpenLeft">
<Label
StyleIdentifier="RecipeHistoryNavButtonLabel"
Align="Center"
Text="{Loc construction-menu-history-select-next-recipe}" />
</Button>
</BoxContainer>
<controls:ListContainer Name="ListViewRecipes" Access="Public" Group="True" Toggle="True" VerticalExpand="True" />
<ScrollContainer Name="GridViewRecipesScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="GridViewRecipes" Columns="5" Access="Public"/>
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">

View file

@ -1,11 +1,15 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Construction.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Construction.UI
{
@ -25,23 +29,31 @@ namespace Content.Client.Construction.UI
bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
ListContainer Recipes { get; }
ListContainer ListViewRecipes { get; }
ScrollContainer GridViewRecipesScrollContainer { get; }
GridContainer GridViewRecipes { get; }
ItemList RecipeStepList { get; }
ScrollContainer RecipesGridScrollContainer { get; }
GridContainer RecipesGrid { get; }
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler<ConstructionMenu.ConstructionMenuListData?> RecipeSelected;
event EventHandler RecipeFavorited;
event EventHandler<bool> BuildButtonToggled;
event EventHandler<bool> EraseButtonToggled;
event EventHandler ClearAllGhosts;
event EventHandler PreviousRecipeInHistoryButtonPressed;
event EventHandler NextRecipeInHistoryButtonPressed;
void ClearRecipeInfo();
void SetRecipeInfo(string name, string description, EntityPrototype? targetPrototype, bool isItem, bool isFavorite);
void ResetPlacement();
void TrySelectCategory(int categoryId);
void TrySelectListViewButton(ProtoId<ConstructionPrototype> constructionProtoId);
void TrySelectGridViewButton(ContainerButton button, ContainerButton? previousButton);
void TogglePreviousRecipeButton(bool enabled);
void ToggleNextRecipeButton(bool enabled);
void UpdateGridViewButtonStyle(BaseButton button, bool selected);
#region Window Control
@ -91,9 +103,9 @@ namespace Content.Client.Construction.UI
Title = Loc.GetString("construction-menu-title");
BuildButton.Text = Loc.GetString("construction-menu-place-ghost");
Recipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData);
Recipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null);
Recipes.GenerateItem += (data, button) =>
ListViewRecipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData);
ListViewRecipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null);
ListViewRecipes.GenerateItem += (data, button) =>
{
if (data is not ConstructionMenuListData (var prototype, var targetPrototype))
return;
@ -124,12 +136,7 @@ namespace Content.Client.Construction.UI
SearchBar.OnTextChanged += _ =>
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
OptionCategories.OnItemSelected += obj =>
{
OptionCategories.SelectId(obj.Id);
SearchBar.SetText(string.Empty);
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[obj.Id]));
};
OptionCategories.OnItemSelected += obj => OnCategorySelected(obj.Id);
BuildButton.Text = Loc.GetString("construction-menu-place-ghost");
BuildButton.OnToggled += args => BuildButtonToggled?.Invoke(this, args.Pressed);
@ -138,6 +145,9 @@ namespace Content.Client.Construction.UI
EraseButton.Text = Loc.GetString("construction-menu-eraser-mode");
EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
NextRecipeInHistoryButton.OnPressed += _ => NextRecipeInHistoryButtonPressed?.Invoke(this, EventArgs.Empty);
PreviousRecipeInHistoryButton.OnPressed += _ => PreviousRecipeInHistoryButtonPressed?.Invoke(this, EventArgs.Empty);
FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty);
MenuGridViewButton.OnPressed += _ =>
@ -150,6 +160,8 @@ namespace Content.Client.Construction.UI
public event EventHandler? RecipeFavorited;
public event EventHandler<bool>? BuildButtonToggled;
public event EventHandler<bool>? EraseButtonToggled;
public event EventHandler? PreviousRecipeInHistoryButtonPressed;
public event EventHandler? NextRecipeInHistoryButtonPressed;
public void ResetPlacement()
{
@ -184,6 +196,102 @@ namespace Content.Client.Construction.UI
RecipeStepList.Clear();
}
public sealed record ConstructionMenuListData(ConstructionPrototype Prototype, EntityPrototype TargetPrototype) : ListData;
public sealed record ConstructionMenuListData(ConstructionPrototype ConstructionProto, EntityPrototype EntityProto) : ListData;
/// <summary>
/// Handle category selection.
/// </summary>
private void OnCategorySelected(int categoryId)
{
OptionCategories.SelectId(categoryId);
SearchBar.SetText(string.Empty);
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[categoryId]));
}
/// <summary>
/// Attempts to select category by its ID.
/// </summary>
/// <returns>Whether it was successful.</returns>
public void TrySelectCategory(int categoryId)
{
// do nothing if category is already selected
if (OptionCategories.SelectedId == categoryId)
{
return;
}
OnCategorySelected(categoryId);
}
/// <summary>
/// Attempts to select specified list view button using recipe ID. Only works when in list view.
/// </summary>
/// <returns>Whether it was successful.</returns>
public void TrySelectListViewButton(ProtoId<ConstructionPrototype> constructionProtoId)
{
var isGridView = GridViewButtonPressed;
if (isGridView)
return;
if (!ListViewRecipes.Data.TryFirstOrDefault(
data => ((ConstructionMenuListData)data).ConstructionProto.ID == constructionProtoId,
out var matchingData))
{
return;
}
ListViewRecipes.Select(matchingData);
}
/// <summary>
/// Attempts to select specified grid view button using recipe ID. Only works when in grid view.
/// </summary>
/// <returns>Whether it was successful.</returns>
public void TrySelectGridViewButton(ContainerButton button, ContainerButton? previousButton)
{
var isGridView = GridViewButtonPressed;
if (!isGridView)
{
return;
}
if (previousButton is not null && previousButton != button)
{
previousButton.Pressed = false;
UpdateGridViewButtonStyle(previousButton, false);
}
button.Pressed = true;
UpdateGridViewButtonStyle(button, true);
}
/// <summary>
/// Toggles previous recipe button on/off.
/// </summary>
/// <param name="enabled">Desired toggle state.</param>
public void TogglePreviousRecipeButton(bool enabled)
{
PreviousRecipeInHistoryButton.Disabled = !enabled;
}
/// <summary>d
/// </summary>
/// <param name="enabled">Desired toggle state.</param>
public void ToggleNextRecipeButton(bool enabled)
{
NextRecipeInHistoryButton.Disabled = !enabled;
}
public void UpdateGridViewButtonStyle(BaseButton button, bool selected)
{
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Children.Single().Modulate = selected ? Color.Green : Color.White;
var buttonColor = selected ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
}
}

View file

@ -41,11 +41,17 @@ namespace Content.Client.Construction.UI
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
// map recipes to grid buttons since grid buttons can't hold metadata
private Dictionary<ProtoId<ConstructionPrototype>, ContainerButton> _gridRecipeButtons = new();
private string _selectedCategory = string.Empty;
private List<RecipeHistoryRecord> _recipeHistory = [];
/// <summary>Index of the selected recipe history record. -1 means no record is selected.</summary>
private int _recipeHistoryIndex = -1;
private const string FavoriteCatName = "construction-category-favorites";
private const string ForAllCategoryName = "construction-category-all";
/// <summary>How many recipes to remember in recipe history. After the limit is reached, older recipes past the limit will get discarded.</summary>
private const int RecipeHistoryLimit = 50;
private bool CraftingAvailable
{
@ -108,7 +114,8 @@ namespace Content.Client.Construction.UI
() => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
_constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
_constructionView.PopulateRecipes += OnViewPopulateRecipes;
_constructionView.RecipeSelected += OnViewRecipeSelected;
_constructionView.RecipeSelected +=
((sender, data) => OnViewRecipeSelected(sender, data?.ConstructionProto));
_constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b);
_constructionView.EraseButtonToggled += (_, b) =>
{
@ -122,6 +129,11 @@ namespace Content.Client.Construction.UI
_constructionView.RecipeFavorited += (_, _) => OnViewFavoriteRecipe();
_constructionView.PreviousRecipeInHistoryButtonPressed +=
(sender, args) => TrySelectRecipeFromHistory(_recipeHistoryIndex - 1);
_constructionView.NextRecipeInHistoryButtonPressed +=
(sender, args) => TrySelectRecipeFromHistory(_recipeHistoryIndex + 1);
SetFavorites(_preferencesManager.Preferences?.ConstructionFavorites ?? []);
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
}
@ -131,57 +143,11 @@ namespace Content.Client.Construction.UI
WindowOpen = args.Pressed;
}
/// <inheritdoc />
public void Dispose()
{
_constructionView.Dispose();
SystemBindingChanged(null);
_systemManager.SystemLoaded -= OnSystemLoaded;
_systemManager.SystemUnloaded -= OnSystemUnloaded;
_placementManager.PlacementChanged -= OnPlacementChanged;
}
private void OnPlacementChanged(object? sender, EventArgs e)
{
_constructionView.ResetPlacement();
}
private void OnViewRecipeSelected(object? sender, ConstructionMenu.ConstructionMenuListData? item)
{
if (item is null)
{
_selected = null;
_constructionView.ClearRecipeInfo();
return;
}
_selected = item.Prototype;
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected);
}
private void OnGridViewRecipeSelected(object? _, ConstructionPrototype? recipe)
{
if (recipe is null)
{
_selected = null;
_constructionView.ClearRecipeInfo();
return;
}
_selected = recipe;
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected);
}
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{
if (_constructionSystem is null)
@ -189,17 +155,18 @@ namespace Content.Client.Construction.UI
var actualRecipes = GetAndSortRecipes(args);
var recipesList = _constructionView.Recipes;
var recipesGrid = _constructionView.RecipesGrid;
var recipesList = _constructionView.ListViewRecipes;
var recipesGrid = _constructionView.GridViewRecipes;
recipesGrid.RemoveAllChildren();
_gridRecipeButtons.Clear();
_constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed;
_constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed;
_constructionView.GridViewRecipesScrollContainer.Visible = _constructionView.GridViewButtonPressed;
_constructionView.ListViewRecipes.Visible = !_constructionView.GridViewButtonPressed;
if (_constructionView.GridViewButtonPressed)
{
recipesList.PopulateList([]);
PopulateGrid(recipesGrid, actualRecipes);
PopulateGridView(recipesGrid, actualRecipes);
}
else
{
@ -207,55 +174,6 @@ namespace Content.Client.Construction.UI
}
}
private void PopulateGrid(GridContainer recipesGrid,
IEnumerable<ConstructionMenu.ConstructionMenuListData> actualRecipes)
{
foreach (var recipe in actualRecipes)
{
var protoView = new EntityPrototypeView()
{
Scale = new Vector2(1.2f),
};
protoView.SetPrototype(recipe.TargetPrototype);
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.Prototype.Name,
ToolTip = recipe.Prototype.Name,
ToggleMode = true,
Children = { protoView },
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.ID, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe.Prototype : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.ID] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
}
private List<ConstructionMenu.ConstructionMenuListData> GetAndSortRecipes((string, string) args)
{
var recipes = new List<ConstructionMenu.ConstructionMenuListData>();
@ -301,66 +219,11 @@ namespace Content.Client.Construction.UI
}
recipes.Sort(
(a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture));
(a, b) => string.Compare(a.ConstructionProto.Name, b.ConstructionProto.Name, StringComparison.InvariantCulture));
return recipes;
}
private void SelectGridButton(BaseButton button, bool select)
{
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Children.Single().Modulate = select ? Color.Green : Color.White;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
private void PopulateCategories(string? selectCategory = null)
{
var uniqueCategories = new HashSet<string>();
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var category = prototype.Category;
if (!string.IsNullOrEmpty(category))
uniqueCategories.Add(category);
}
var isFavorites = _favoritedRecipes.Count > 0;
var categoriesArray = new string[isFavorites ? uniqueCategories.Count + 2 : uniqueCategories.Count + 1];
// hard-coded to show all recipes
var idx = 0;
categoriesArray[idx++] = ForAllCategoryName;
// hard-coded to show favorites if it need
if (isFavorites)
{
categoriesArray[idx++] = FavoriteCatName;
}
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
foreach (var cat in sortedProtoCategories)
{
categoriesArray[idx++] = cat;
}
_constructionView.OptionCategories.Clear();
for (var i = 0; i < categoriesArray.Length; i++)
{
_constructionView.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i);
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
_constructionView.OptionCategories.SelectId(i);
}
_constructionView.Categories = categoriesArray;
}
private void PopulateInfo(ConstructionPrototype? prototype)
{
if (_constructionSystem is null)
@ -450,6 +313,285 @@ namespace Content.Client.Construction.UI
_constructionView.BuildButtonPressed = pressed;
}
private void OnViewRecipeSelected(object? sender, ConstructionPrototype? constructionProto, bool appendToHistory = true)
{
if (constructionProto is null)
{
_selected = null;
_constructionView.ClearRecipeInfo();
return;
}
_selected = constructionProto;
if (appendToHistory)
{
AppendToRecipeHistory(_selected, _constructionView.OptionCategories.SelectedId);
}
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected);
}
#region View mode: Grid
private void PopulateGridView(GridContainer recipesGrid,
IEnumerable<ConstructionMenu.ConstructionMenuListData> actualRecipes)
{
foreach (var recipe in actualRecipes)
{
var protoView = new EntityPrototypeView()
{
Scale = new Vector2(1.2f),
};
protoView.SetPrototype(recipe.EntityProto);
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.ConstructionProto.Name,
ToolTip = recipe.ConstructionProto.Name,
ToggleMode = true,
Children = { protoView },
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += _ =>
OnGridViewButtonToggled(recipe.ConstructionProto, itemButton);
recipesGrid.AddChild(itemButtonPanelContainer);
_gridRecipeButtons[recipe.ConstructionProto.ID] = itemButton;
var isCurrentButtonSelected = _selected == recipe.ConstructionProto;
itemButton.Pressed = isCurrentButtonSelected;
_constructionView.UpdateGridViewButtonStyle(itemButton, isCurrentButtonSelected);
}
}
/// <summary>
/// Handles grid view button toggle.
/// </summary>
private void OnGridViewButtonToggled(ConstructionPrototype constructionProto, ContainerButton button)
{
ContainerButton? previousButton = null;
if (_selected != null)
{
_gridRecipeButtons.TryGetValue(_selected, out previousButton);
}
_constructionView.TrySelectGridViewButton(button, previousButton);
OnViewRecipeSelected(this, constructionProto);
}
#endregion
#region Categories
private void PopulateCategories(string? selectCategory = null)
{
var uniqueCategories = new HashSet<string>();
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var category = prototype.Category;
if (!string.IsNullOrEmpty(category))
uniqueCategories.Add(category);
}
var isFavorites = _favoritedRecipes.Count > 0;
var categoriesArray = new string[isFavorites ? uniqueCategories.Count + 2 : uniqueCategories.Count + 1];
// hard-coded to show all recipes
var idx = 0;
categoriesArray[idx++] = ForAllCategoryName;
// hard-coded to show favorites if it need
if (isFavorites)
{
categoriesArray[idx++] = FavoriteCatName;
}
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
foreach (var cat in sortedProtoCategories)
{
categoriesArray[idx++] = cat;
}
_constructionView.OptionCategories.Clear();
for (var i = 0; i < categoriesArray.Length; i++)
{
_constructionView.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i);
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
_constructionView.OptionCategories.SelectId(i);
}
_constructionView.Categories = categoriesArray;
// keep category indices in recipe history aligned to categories
for (var i = 0; i < _recipeHistory.Count; i++)
{
var entry = _recipeHistory[i];
// category index = category display ID, see "OptionCategories.AddItem" above.
if (entry.CategoryId == 0)
{
// 0 = "all" category. these should remain as is.
continue;
}
var newCategoryDisplayId = Array.IndexOf(categoriesArray, entry.Category);
if (newCategoryDisplayId == -1)
{
// set to "all" if category wasn't found
newCategoryDisplayId = 0;
}
if (newCategoryDisplayId != entry.CategoryId)
{
entry.CategoryId = newCategoryDisplayId;
_recipeHistory[i] = entry;
}
}
}
#endregion
#region Recipe history
/// <summary>
/// Adds recipe to recipe history.
///
/// If the recipe is duplicate of previous recipe in the history, it will be ignored.
/// If there are recipes that follow, they will be discarded.
/// </summary>
private void AppendToRecipeHistory(ConstructionPrototype constructionProto, int categoryId)
{
var previous = _recipeHistory.ElementAtOrDefault(_recipeHistoryIndex);
// check if previous is the same as current
if (previous != default && previous.ConstructionProtoId.Id == constructionProto.ID)
{
return;
}
// discard recipes that follow, if any
if (_recipeHistory.Count > _recipeHistoryIndex + 1)
{
var removeFromIdx = _recipeHistoryIndex + 1;
var numElementsToRemove = _recipeHistory.Count - (removeFromIdx + 1) + 1;
_recipeHistory.RemoveRange(removeFromIdx, numElementsToRemove);
}
_recipeHistory.Add(new RecipeHistoryRecord()
{
ConstructionProtoId = constructionProto,
Category = constructionProto.Category,
CategoryId = categoryId
});
_recipeHistoryIndex++;
if (RecipeHistoryLimit > 0 && _recipeHistory.Count > RecipeHistoryLimit)
{
_recipeHistory.RemoveAt(0);
_recipeHistoryIndex--;
ClampRecipeHistoryIndex();
}
SyncRecipeHistoryButtons();
}
/// <summary>
/// Attempts to select recipe from history with specified index. The index is clamped first to be within history bounds.
/// </summary>
private void TrySelectRecipeFromHistory(int historyIndex)
{
_recipeHistoryIndex = historyIndex;
if (ClampRecipeHistoryIndex() < 0)
{
return;
}
var record = _recipeHistory[_recipeHistoryIndex];
TrySelectRecipeFromHistory(record);
}
/// <summary>
/// Attempts to select recipe from history using a history entry.
/// </summary>
private void TrySelectRecipeFromHistory(RecipeHistoryRecord record)
{
if(!_prototypeManager.TryIndex(record.ConstructionProtoId, out var constructionProto))
return;
_constructionView.TrySelectCategory(record.CategoryId);
var isGridView = _constructionView.GridViewButtonPressed;
if (isGridView)
{
if (_gridRecipeButtons.TryGetValue(constructionProto.ID, out var button))
{
// use the press handler here it does exactly what we want.
OnGridViewButtonToggled(constructionProto, button);
}
}
else
{
_constructionView.TrySelectListViewButton(constructionProto.ID);
}
OnViewRecipeSelected(this, constructionProto, appendToHistory:false);
SyncRecipeHistoryButtons();
}
/// <summary>
/// Toggle recipe history buttons based to recipy history state.
/// </summary>
private void SyncRecipeHistoryButtons()
{
if (ClampRecipeHistoryIndex() < 0)
{
_constructionView.TogglePreviousRecipeButton(false);
_constructionView.ToggleNextRecipeButton(false);
return;
}
_constructionView.TogglePreviousRecipeButton(_recipeHistoryIndex > 0);
_constructionView.ToggleNextRecipeButton(_recipeHistoryIndex < _recipeHistory.Count - 1);
}
/// <summary>
/// Clamps recipe history index to bounds of the recipes history list and writes it.
/// If there are no history entries, the index is set to -1.
/// </summary>
/// <returns>Resulting index.</returns>
private int ClampRecipeHistoryIndex()
{
switch (_recipeHistory.Count)
{
case 0:
_recipeHistoryIndex = -1;
break;
case >0:
_recipeHistoryIndex = int.Clamp(_recipeHistoryIndex, 0, _recipeHistory.Count - 1);
break;
}
return _recipeHistoryIndex;
}
#endregion
#region Ghosts
private void UpdateGhostPlacement()
{
if (_selected == null)
@ -473,17 +615,9 @@ namespace Content.Client.Construction.UI
_constructionView.BuildButtonPressed = true;
}
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem system)
SystemBindingChanged(system);
}
#endregion
private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem)
SystemBindingChanged(null);
}
#region Favorites
private void OnViewFavoriteRecipe()
{
@ -527,6 +661,32 @@ namespace Content.Client.Construction.UI
PopulateCategories(_selectedCategory);
}
#endregion
/// <inheritdoc />
public void Dispose()
{
_constructionView.Dispose();
SystemBindingChanged(null);
_systemManager.SystemLoaded -= OnSystemLoaded;
_systemManager.SystemUnloaded -= OnSystemUnloaded;
_placementManager.PlacementChanged -= OnPlacementChanged;
}
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem system)
SystemBindingChanged(system);
}
private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem)
SystemBindingChanged(null);
}
private void SystemBindingChanged(ConstructionSystem? newSystem)
{
if (newSystem is null)
@ -641,3 +801,42 @@ namespace Content.Client.Construction.UI
}
}
}
/// <summary>
/// Represents a construction menu recipe history entry.
/// </summary>
internal struct RecipeHistoryRecord : IEquatable<RecipeHistoryRecord>
{
public ProtoId<ConstructionPrototype> ConstructionProtoId;
/// <summary>
/// An ID to uniquely indentify category without relying on its label.
/// </summary>
public int CategoryId;
public string Category;
public bool Equals(RecipeHistoryRecord other)
{
return this == other;
}
public override bool Equals(object? obj)
{
return obj is RecipeHistoryRecord other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(ConstructionProtoId.GetHashCode(), CategoryId.GetHashCode());
}
public static bool operator == (RecipeHistoryRecord left, RecipeHistoryRecord right)
{
return left.ConstructionProtoId.Equals(right.ConstructionProtoId)
&& left.CategoryId == right.CategoryId;
}
public static bool operator != (RecipeHistoryRecord left, RecipeHistoryRecord right)
{
return !left.ConstructionProtoId.Equals(right.ConstructionProtoId);
}
}

View file

@ -0,0 +1,26 @@
using Content.Client.Stylesheets;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client.Construction.UI;
[CommonSheetlet]
public sealed class ConstructionMenuSheetlet : Sheetlet<PalettedStylesheet>
{
public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
{
return
[
E<Label>()
.Identifier("RecipeHistoryNavButtonLabel")
.Font(sheet.BaseFont.GetFont(8))
.FontColor(Color.White),
E<Label>()
.Identifier("RecipeHistoryNavButtonLabel")
.PseudoDisabled()
.Font(sheet.BaseFont.GetFont(8))
.FontColor(Color.Gray),
];
}
}

View file

@ -7,3 +7,5 @@ construction-menu-eraser-mode = Eraser Mode
construction-menu-craft = Craft
construction-menu-search = Search
construction-menu-grid-view = Grid View
construction-menu-history-select-previous-recipe = ◄ Back
construction-menu-history-select-next-recipe = Next ►