From 68e09ade73320aac993ad4a620a49d7b3ba61aa1 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 23 Jan 2023 01:17:50 -0500 Subject: [PATCH 1/2] feature: Add display options for the Shopping List window. --- Yafc.UI/ImGui/ImGuiUtils.cs | 16 ++- Yafc/Utils/Preferences.cs | 4 + Yafc/Windows/MainScreen.PageListSearch.cs | 2 +- Yafc/Windows/ShoppingListScreen.cs | 110 +++++++++++++++--- .../ProductionTable/ProductionTableView.cs | 22 +--- changelog.txt | 3 + 6 files changed, 113 insertions(+), 44 deletions(-) diff --git a/Yafc.UI/ImGui/ImGuiUtils.cs b/Yafc.UI/ImGui/ImGuiUtils.cs index 3b289fe3..f654d85c 100644 --- a/Yafc.UI/ImGui/ImGuiUtils.cs +++ b/Yafc.UI/ImGui/ImGuiUtils.cs @@ -215,13 +215,21 @@ public static bool BuildCheckBox(this ImGui gui, string text, bool value, out bo return false; } - public static bool BuildRadioButton(this ImGui gui, string option, bool selected, SchemeColor color = SchemeColor.None) { + public static ButtonEvent BuildRadioButton(this ImGui gui, string option, bool selected, SchemeColor textColor = SchemeColor.None, bool enabled = true) { + if (textColor == SchemeColor.None) { + textColor = enabled ? SchemeColor.PrimaryText : SchemeColor.PrimaryTextFaint; + } using (gui.EnterRow()) { - gui.BuildIcon(selected ? Icon.RadioCheck : Icon.RadioEmpty, 1.5f, color); - gui.BuildText(option, TextBlockDisplayStyle.WrappedText with { Color = color }); + gui.BuildIcon(selected ? Icon.RadioCheck : Icon.RadioEmpty, 1.5f, textColor); + gui.BuildText(option, TextBlockDisplayStyle.WrappedText with { Color = textColor }); + } + if (!enabled) { + return ButtonEvent.None; } - return !selected && gui.OnClick(gui.lastRect); + ButtonEvent click = gui.BuildButton(gui.lastRect, SchemeColor.None, SchemeColor.None); + if (click == ButtonEvent.Click && selected) { return ButtonEvent.None; } + return click; } public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList options, int selected, out int newSelected, SchemeColor color = SchemeColor.None) { diff --git a/Yafc/Utils/Preferences.cs b/Yafc/Utils/Preferences.cs index b9284697..6b8788c2 100644 --- a/Yafc/Utils/Preferences.cs +++ b/Yafc/Utils/Preferences.cs @@ -68,6 +68,10 @@ public void Save() { /// - Your system has a very old graphics card that is not supported by Windows DX12 /// public bool forceSoftwareRenderer { get; set; } = false; + /// + /// An opaque integer that the shopping list uses to store its display options. See the ShoppingListScreen properties that read and write this value. + /// + public int shoppingDisplayState { get; set; } = 3; public void AddProject(string dataPath, string modsPath, string projectPath, bool expensiveRecipes, bool netProduction) { recentProjects = recentProjects.Where(x => string.Compare(projectPath, x.path, StringComparison.InvariantCultureIgnoreCase) != 0) diff --git a/Yafc/Windows/MainScreen.PageListSearch.cs b/Yafc/Windows/MainScreen.PageListSearch.cs index f9c443e2..596f1502 100644 --- a/Yafc/Windows/MainScreen.PageListSearch.cs +++ b/Yafc/Windows/MainScreen.PageListSearch.cs @@ -83,7 +83,7 @@ void buildCheckbox(ImGui gui, string text, ref bool isChecked) { void buildRadioButton(ImGui gui, string text, SearchNameMode thisValue) { // All checkboxes except PageSearchOption.PageName search object names. bool isObjectNameSearching = checkboxValues[1..].Any(x => x); - if (gui.BuildRadioButton(text, searchNameMode == thisValue, isObjectNameSearching ? SchemeColor.PrimaryText : SchemeColor.PrimaryTextFaint) && isObjectNameSearching) { + if (gui.BuildRadioButton(text, searchNameMode == thisValue, enabled: isObjectNameSearching)) { searchNameMode = thisValue; updatePageList(); } diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index 14b2363e..6a5a19be 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Yafc.Blueprints; @@ -7,13 +8,32 @@ namespace Yafc { public class ShoppingListScreen : PseudoScreen { - private static readonly ShoppingListScreen Instance = new ShoppingListScreen(); - + private enum DisplayState { Total, Built, Missing } private readonly VirtualScrollList<(FactorioObject, float)> list; private float shoppingCost, totalBuildings, totalModules; private bool decomposed = false; + private static DisplayState displayState { + get => (DisplayState)(Preferences.Instance.shoppingDisplayState >> 1); + set { + Preferences.Instance.shoppingDisplayState = ((int)value) << 1 | (Preferences.Instance.shoppingDisplayState & 1); + Preferences.Instance.Save(); + } + } + private static bool assumeAdequate { + get => (Preferences.Instance.shoppingDisplayState & 1) != 0; + set { + Preferences.Instance.shoppingDisplayState = (Preferences.Instance.shoppingDisplayState & ~1) | (value ? 1 : 0); + Preferences.Instance.Save(); + } + } + + private readonly List recipes; - private ShoppingListScreen() => list = new VirtualScrollList<(FactorioObject, float)>(30f, new Vector2(float.PositiveInfinity, 2), ElementDrawer); + private ShoppingListScreen(List recipes) { + list = new VirtualScrollList<(FactorioObject, float)>(30f, new Vector2(float.PositiveInfinity, 2), ElementDrawer); + this.recipes = recipes; + RebuildData(); + } private void ElementDrawer(ImGui gui, (FactorioObject obj, float count) element, int index) { using (gui.EnterRow()) { @@ -23,24 +43,52 @@ private void ElementDrawer(ImGui gui, (FactorioObject obj, float count) element, _ = gui.BuildFactorioObjectButtonBackground(gui.lastRect, element.obj); } - public static void Show(Dictionary counts) { - float cost = 0f, buildings = 0f, modules = 0f; - Instance.decomposed = false; - Instance.list.data = counts.Select(x => (x.Key, Value: (float)x.Value)).OrderByDescending(x => x.Value).ToArray(); - foreach (var (obj, count) in Instance.list.data) { - if (obj is Entity) { - buildings += count; + public static void Show(List recipes) => _ = MainScreen.Instance.ShowPseudoScreen(new ShoppingListScreen(recipes)); + + private void RebuildData() { + decomposed = false; + + // Count buildings and modules + Dictionary counts = []; + foreach (RecipeRow recipe in recipes) { + if (recipe.entity != null) { + FactorioObject shopItem = recipe.entity.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity; + _ = counts.TryGetValue(shopItem, out int prev); + int builtCount = recipe.builtBuildings ?? (assumeAdequate ? MathUtils.Ceil(recipe.buildingCount) : 0); + int displayCount = displayState switch { + DisplayState.Total => MathUtils.Ceil(recipe.buildingCount), + DisplayState.Built => builtCount, + DisplayState.Missing => MathUtils.Ceil(Math.Max(recipe.buildingCount - builtCount, 0)), + _ => throw new InvalidOperationException(nameof(displayState) + " has an unrecognized value.") + }; + counts[shopItem] = prev + displayCount; + if (recipe.usedModules.modules != null) { + foreach ((Module module, int moduleCount, bool beacon) in recipe.usedModules.modules) { + if (!beacon) { + _ = counts.TryGetValue(module, out prev); + counts[module] = prev + displayCount * moduleCount; + } + } + } } - else if (obj is Module module) { + } + list.data = [.. counts.Where(x => x.Value > 0).Select(x => (x.Key, Value: (float)x.Value)).OrderByDescending(x => x.Value)]; + + // Summarize building requirements + float cost = 0f, buildings = 0f, modules = 0f; + decomposed = false; + foreach ((FactorioObject obj, float count) in list.data) { + if (obj is Module module) { modules += count; } - + else if (obj is Entity or Item) { + buildings += count; + } cost += obj.Cost() * count; } - Instance.shoppingCost = cost; - Instance.totalBuildings = buildings; - Instance.totalModules = modules; - _ = MainScreen.Instance.ShowPseudoScreen(Instance); + shoppingCost = cost; + totalBuildings = buildings; + totalModules = modules; } public override void Build(ImGui gui) { @@ -48,6 +96,32 @@ public override void Build(ImGui gui) { gui.BuildText( "Total cost of all objects: " + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None, "¥") + ", buildings: " + DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None) + ", modules: " + DataUtils.FormatAmount(totalModules, UnitOfMeasure.None), TextBlockDisplayStyle.Centered); + using (gui.EnterRow()) { + if (gui.BuildRadioButton("Total buildings", displayState == DisplayState.Total).WithTooltip(gui, "Display the total number of buildings required, ignoring the built building count.")) { + displayState = DisplayState.Total; + RebuildData(); + } + if (gui.BuildRadioButton("Built buildings", displayState == DisplayState.Built).WithTooltip(gui, "Display the number of buildings that are reported in built building count.")) { + displayState = DisplayState.Built; + RebuildData(); + } + if (gui.BuildRadioButton("Missing buildings", displayState == DisplayState.Missing).WithTooltip(gui, "Display the number of additional buildings that need to be built.")) { + displayState = DisplayState.Missing; + RebuildData(); + } + } + using (gui.EnterRow()) { + SchemeColor textColor = displayState == DisplayState.Total ? SchemeColor.PrimaryTextFaint : SchemeColor.PrimaryText; + gui.BuildText("When not specified, assume:", TextBlockDisplayStyle.Default(textColor), topOffset: .15f); + if (gui.BuildRadioButton("No buildings", !assumeAdequate, enabled: displayState != DisplayState.Total).WithTooltip(gui, "When the built building count is not specified, behave as if it was set to 0.")) { + assumeAdequate = false; + RebuildData(); + } + if (gui.BuildRadioButton("Enough buildings", assumeAdequate, enabled: displayState != DisplayState.Total).WithTooltip(gui, "When the built building count is not specified, behave as if it matches the required building count.")) { + assumeAdequate = true; + RebuildData(); + } + } gui.AllocateSpacing(1f); list.Build(gui); using (gui.EnterRow(allocator: RectAllocator.RightRow)) { @@ -99,7 +173,7 @@ private void ExportBlueprintDropdown(ImGui gui) { private Recipe? FindSingleProduction(Recipe[] production) { Recipe? current = null; - foreach (var recipe in production) { + foreach (Recipe recipe in production) { if (recipe.IsAccessible()) { if (current != null) { return null; diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index 087ed9b5..80f57dfb 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -1163,27 +1163,7 @@ private List GetRecipesRecursive(RecipeRow recipeRoot) { return list; } - private void BuildShoppingList(RecipeRow? recipeRoot) { - Dictionary shopList = []; - var recipes = recipeRoot == null ? GetRecipesRecursive() : GetRecipesRecursive(recipeRoot); - foreach (var recipe in recipes) { - if (recipe.entity != null) { - FactorioObject shopItem = recipe.entity.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity; - _ = shopList.TryGetValue(shopItem, out int prev); - int count = MathUtils.Ceil(recipe.builtBuildings ?? recipe.buildingCount); - shopList[shopItem] = prev + count; - if (recipe.usedModules.modules != null) { - foreach (var module in recipe.usedModules.modules) { - if (!module.beacon) { - _ = shopList.TryGetValue(module.module, out prev); - shopList[module.module] = prev + (count * module.count); - } - } - } - } - } - ShoppingListScreen.Show(shopList); - } + private void BuildShoppingList(RecipeRow? recipeRoot) => ShoppingListScreen.Show(recipeRoot == null ? GetRecipesRecursive() : GetRecipesRecursive(recipeRoot)); private void BuildBeltInserterInfo(ImGui gui, float amount, float buildingCount) { var prefs = Project.current.preferences; diff --git a/changelog.txt b/changelog.txt index ffa6ac1e..1e7f95c4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,8 +20,11 @@ Date: Features: - Add OSX-arm64 build. - Display link warnings in both the tooltips and the dropdowns. + - Add additional ways of counting buildings and modules when displaying the shopping list. Bugfixes: - Fixed recipes now become accessible when their crafter does. + Internal changes: + - Allow tooltips to be displayed when hovering over radio buttons. ---------------------------------------------------------------------------------------------------------------------- Version: 0.9.1 Date: September 8th 2024 From 4e2c9681e7bc2c11c38489b4b8e86d848ffd0411 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sun, 21 Jul 2024 23:38:44 -0400 Subject: [PATCH 2/2] refactor: Use BuildRadioGroup for the Shopping List options. --- Yafc.UI/ImGui/ImGuiUtils.cs | 14 ++++++++++++-- Yafc/Windows/ShoppingListScreen.cs | 28 ++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Yafc.UI/ImGui/ImGuiUtils.cs b/Yafc.UI/ImGui/ImGuiUtils.cs index f654d85c..9aafa897 100644 --- a/Yafc.UI/ImGui/ImGuiUtils.cs +++ b/Yafc.UI/ImGui/ImGuiUtils.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using SDL2; @@ -232,10 +233,19 @@ public static ButtonEvent BuildRadioButton(this ImGui gui, string option, bool s return click; } - public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList options, int selected, out int newSelected, SchemeColor color = SchemeColor.None) { + public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList options, int selected, out int newSelected, + SchemeColor textColor = SchemeColor.None, bool enabled = true) + => gui.BuildRadioGroup([.. options.Select(o => (o, (string?)null))], selected, out newSelected, textColor, enabled); + + public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<(string option, string? tooltip)> options, int selected, + out int newSelected, SchemeColor textColor = SchemeColor.None, bool enabled = true) { newSelected = selected; for (int i = 0; i < options.Count; i++) { - if (BuildRadioButton(gui, options[i], selected == i, color)) { + ButtonEvent evt = BuildRadioButton(gui, options[i].option, selected == i, textColor, enabled); + if (!string.IsNullOrEmpty(options[i].tooltip)) { + evt.WithTooltip(gui, options[i].tooltip!); + } + if (evt) { newSelected = i; } } diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index 6a5a19be..9b042e89 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -91,34 +91,30 @@ private void RebuildData() { totalModules = modules; } + private static readonly (string, string?)[] displayStateOptions = [ + ("Total buildings", "Display the total number of buildings required, ignoring the built building count."), + ("Built buildings", "Display the number of buildings that are reported in built building count."), + ("Missing buildings", "Display the number of additional buildings that need to be built.")]; + private static readonly (string, string?)[] assumeAdequateOptions = [ + ("No buildings", "When the built building count is not specified, behave as if it was set to 0."), + ("Enough buildings", "When the built building count is not specified, behave as if it matches the required building count.")]; + public override void Build(ImGui gui) { BuildHeader(gui, "Shopping list"); gui.BuildText( "Total cost of all objects: " + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None, "¥") + ", buildings: " + DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None) + ", modules: " + DataUtils.FormatAmount(totalModules, UnitOfMeasure.None), TextBlockDisplayStyle.Centered); using (gui.EnterRow()) { - if (gui.BuildRadioButton("Total buildings", displayState == DisplayState.Total).WithTooltip(gui, "Display the total number of buildings required, ignoring the built building count.")) { - displayState = DisplayState.Total; - RebuildData(); - } - if (gui.BuildRadioButton("Built buildings", displayState == DisplayState.Built).WithTooltip(gui, "Display the number of buildings that are reported in built building count.")) { - displayState = DisplayState.Built; - RebuildData(); - } - if (gui.BuildRadioButton("Missing buildings", displayState == DisplayState.Missing).WithTooltip(gui, "Display the number of additional buildings that need to be built.")) { - displayState = DisplayState.Missing; + if (gui.BuildRadioGroup(displayStateOptions, (int)displayState, out int newSelected)) { + displayState = (DisplayState)newSelected; RebuildData(); } } using (gui.EnterRow()) { SchemeColor textColor = displayState == DisplayState.Total ? SchemeColor.PrimaryTextFaint : SchemeColor.PrimaryText; gui.BuildText("When not specified, assume:", TextBlockDisplayStyle.Default(textColor), topOffset: .15f); - if (gui.BuildRadioButton("No buildings", !assumeAdequate, enabled: displayState != DisplayState.Total).WithTooltip(gui, "When the built building count is not specified, behave as if it was set to 0.")) { - assumeAdequate = false; - RebuildData(); - } - if (gui.BuildRadioButton("Enough buildings", assumeAdequate, enabled: displayState != DisplayState.Total).WithTooltip(gui, "When the built building count is not specified, behave as if it matches the required building count.")) { - assumeAdequate = true; + if (gui.BuildRadioGroup(assumeAdequateOptions, assumeAdequate ? 1 : 0, out int newSelected, enabled: displayState != DisplayState.Total)) { + assumeAdequate = newSelected == 1; RebuildData(); } }