diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml b/Content.Client/Construction/UI/ConstructionMenu.xaml index 4020ca4332..29c1dfaf5b 100644 --- a/Content.Client/Construction/UI/ConstructionMenu.xaml +++ b/Content.Client/Construction/UI/ConstructionMenu.xaml @@ -2,13 +2,36 @@ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"> - + - - - + + + + + + + diff --git a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs index d94150d5cd..ba1c36b4de 100644 --- a/Content.Client/Construction/UI/ConstructionMenu.xaml.cs +++ b/Content.Client/Construction/UI/ConstructionMenu.xaml.cs @@ -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 RecipeSelected; event EventHandler RecipeFavorited; event EventHandler BuildButtonToggled; event EventHandler 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 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? BuildButtonToggled; public event EventHandler? 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; + + /// + /// Handle category selection. + /// + private void OnCategorySelected(int categoryId) + { + OptionCategories.SelectId(categoryId); + SearchBar.SetText(string.Empty); + PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[categoryId])); + } + + /// + /// Attempts to select category by its ID. + /// + /// Whether it was successful. + public void TrySelectCategory(int categoryId) + { + // do nothing if category is already selected + if (OptionCategories.SelectedId == categoryId) + { + return; + } + + OnCategorySelected(categoryId); + } + + /// + /// Attempts to select specified list view button using recipe ID. Only works when in list view. + /// + /// Whether it was successful. + public void TrySelectListViewButton(ProtoId constructionProtoId) + { + var isGridView = GridViewButtonPressed; + if (isGridView) + return; + + if (!ListViewRecipes.Data.TryFirstOrDefault( + data => ((ConstructionMenuListData)data).ConstructionProto.ID == constructionProtoId, + out var matchingData)) + { + return; + } + + ListViewRecipes.Select(matchingData); + } + + /// + /// Attempts to select specified grid view button using recipe ID. Only works when in grid view. + /// + /// Whether it was successful. + 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); + } + + /// + /// Toggles previous recipe button on/off. + /// + /// Desired toggle state. + public void TogglePreviousRecipeButton(bool enabled) + { + PreviousRecipeInHistoryButton.Disabled = !enabled; + } + + /// d + /// + /// Desired toggle state. + 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 }; + } } } diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index 48c7ba93b3..450c3d1eaa 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -41,11 +41,17 @@ namespace Content.Client.Construction.UI private ConstructionSystem? _constructionSystem; private ConstructionPrototype? _selected; private List _favoritedRecipes = []; - private readonly Dictionary _recipeButtons = new(); + // map recipes to grid buttons since grid buttons can't hold metadata + private Dictionary, ContainerButton> _gridRecipeButtons = new(); private string _selectedCategory = string.Empty; + private List _recipeHistory = []; + /// Index of the selected recipe history record. -1 means no record is selected. + private int _recipeHistoryIndex = -1; private const string FavoriteCatName = "construction-category-favorites"; private const string ForAllCategoryName = "construction-category-all"; + /// How many recipes to remember in recipe history. After the limit is reached, older recipes past the limit will get discarded. + private const int RecipeHistoryLimit = 50; private bool CraftingAvailable { @@ -108,7 +114,8 @@ namespace Content.Client.Construction.UI () => _uiManager.GetActiveUIWidget().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; } - /// - 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 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 GetAndSortRecipes((string, string) args) { var recipes = new List(); @@ -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(); - - foreach (var prototype in _prototypeManager.EnumeratePrototypes()) - { - 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 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); + } + } + + /// + /// Handles grid view button toggle. + /// + 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(); + + foreach (var prototype in _prototypeManager.EnumeratePrototypes()) + { + 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 + + /// + /// 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. + /// + 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(); + } + + /// + /// Attempts to select recipe from history with specified index. The index is clamped first to be within history bounds. + /// + private void TrySelectRecipeFromHistory(int historyIndex) + { + _recipeHistoryIndex = historyIndex; + if (ClampRecipeHistoryIndex() < 0) + { + return; + } + + var record = _recipeHistory[_recipeHistoryIndex]; + TrySelectRecipeFromHistory(record); + } + + /// + /// Attempts to select recipe from history using a history entry. + /// + 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(); + } + + /// + /// Toggle recipe history buttons based to recipy history state. + /// + private void SyncRecipeHistoryButtons() + { + if (ClampRecipeHistoryIndex() < 0) + { + _constructionView.TogglePreviousRecipeButton(false); + _constructionView.ToggleNextRecipeButton(false); + return; + } + + _constructionView.TogglePreviousRecipeButton(_recipeHistoryIndex > 0); + _constructionView.ToggleNextRecipeButton(_recipeHistoryIndex < _recipeHistory.Count - 1); + } + + /// + /// 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. + /// + /// Resulting index. + 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 + + /// + 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 } } } + +/// +/// Represents a construction menu recipe history entry. +/// +internal struct RecipeHistoryRecord : IEquatable +{ + public ProtoId ConstructionProtoId; + + /// + /// An ID to uniquely indentify category without relying on its label. + /// + 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); + } +} diff --git a/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs b/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs new file mode 100644 index 0000000000..bc59d4b5eb --- /dev/null +++ b/Content.Client/Construction/UI/ConstructionMenuSheetlet.cs @@ -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 +{ + public override StyleRule[] GetRules(PalettedStylesheet sheet, object config) + { + return + [ + E