diff --git a/YAFC/Widgets/DataGrid.cs b/YAFC/Widgets/DataGrid.cs index 96f70d94..5795637f 100644 --- a/YAFC/Widgets/DataGrid.cs +++ b/YAFC/Widgets/DataGrid.cs @@ -89,6 +89,14 @@ private void BuildHeaderResizer(ImGui gui, DataColumn column, Rect rect) } } + private void CalculateWidth(ImGui gui) { + var x = 0f; + foreach (var column in columns) { + x += column.width + spacing; + } + width = MathF.Max(x + 0.2f - spacing, gui.width - 1f); + } + public void BuildHeader(ImGui gui) { var spacing = innerPadding.left + innerPadding.right; var x = 0f; @@ -111,7 +119,7 @@ public void BuildHeader(ImGui gui) { } } } - width = MathF.Max(x + 0.2f - spacing, gui.width - 1f); + CalculateWidth(gui); var separator = gui.AllocateRect(x, 0.1f); if (gui.isBuilding) { @@ -141,6 +149,7 @@ public Rect BuildRow(ImGui gui, TData element, float startX = 0f) { buildGroup.Complete(); } + CalculateWidth(gui); var rect = gui.lastRect; var bottom = gui.lastRect.Bottom; if (gui.isBuilding) diff --git a/YAFC/Widgets/MainScreenTabBar.cs b/YAFC/Widgets/MainScreenTabBar.cs index 740010ad..2393678e 100644 --- a/YAFC/Widgets/MainScreenTabBar.cs +++ b/YAFC/Widgets/MainScreenTabBar.cs @@ -41,7 +41,7 @@ private void BuildContents(ImGui gui) { changePageTo = prevPage; changePage = isActive ? 1 : 2; } - project.RecordUndo(true).displayPages.RemoveAt(i); + screen.ClosePage(pageGuid); i--; } } diff --git a/YAFC/Windows/MainScreen.cs b/YAFC/Windows/MainScreen.cs index 68a102e7..9c80c660 100644 --- a/YAFC/Windows/MainScreen.cs +++ b/YAFC/Windows/MainScreen.cs @@ -12,6 +12,8 @@ namespace YAFC { public class MainScreen : WindowMain, IKeyboardFocus, IProgress<(string, string)> { + ///Unique ID for the Summary page + public static readonly Guid SummaryGuid = Guid.Parse("9bdea333-4be2-4be3-b708-b36a64672a40"); public static MainScreen Instance { get; private set; } private readonly ObjectTooltip objectTooltip = new ObjectTooltip(); private readonly List pseudoScreens = new List(); @@ -28,6 +30,7 @@ public class MainScreen : WindowMain, IKeyboardFocus, IProgress<(string, string) private ProjectPage _secondaryPage; public ProjectPage secondaryPage => _secondaryPage; private ProjectPageView secondaryPageView; + private readonly SummaryView summaryView; private bool analysisUpdatePending; private SearchQuery pageSearch; @@ -40,9 +43,11 @@ public class MainScreen : WindowMain, IKeyboardFocus, IProgress<(string, string) private readonly Dictionary secondaryPageViews = new Dictionary(); public MainScreen(int display, Project project) : base(default) { + summaryView = new SummaryView(); RegisterPageView(new ProductionTableView()); RegisterPageView(new AutoPlannerView()); RegisterPageView(new ProductionSummaryView()); + RegisterPageView(summaryView); searchGui = new ImGui(BuildSearch, new Padding(1f)) { boxShadow = RectangleBorder.Thin, boxColor = SchemeColor.Background }; Instance = this; tabBar = new MainScreenTabBar(this); @@ -71,10 +76,17 @@ private void SetProject(Project project) { if (project.displayPages.Count == 0) project.displayPages.Add(project.pages[0].guid); + // Hack to activate all page solvers for the summary view + foreach (var page in project.pages) { + page.SetActive(true); + page.SetActive(false); + } + SetActivePage(project.FindPage(project.displayPages[0])); project.metaInfoChanged += ProjectOnMetaInfoChanged; project.settings.changed += ProjectSettingsChanged; InputSystem.Instance.SetDefaultKeyboardFocus(this); + summaryView.SetProject(project); } private void ProjectSettingsChanged(bool visualOnly) { @@ -338,6 +350,9 @@ private void SettingsDropdown(ImGui gui) { if (gui.BuildContextMenuButton("Preferences") && gui.CloseDropdown()) PreferencesScreen.Show(); + if (gui.BuildContextMenuButton("Summary") && gui.CloseDropdown()) + ShowSummaryTab(); + if (gui.BuildContextMenuButton("Never Enough Items Explorer", "Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_N)) && gui.CloseDropdown()) ShowNeie(); @@ -443,6 +458,24 @@ public void ClosePseudoScreen(PseudoScreen screen) { rootGui.Rebuild(); } + public void ClosePage(Guid page) { + project.RecordUndo(true).displayPages.Remove(page); + } + + public void ShowSummaryTab() { + + ProjectPage summaryPage = project.FindPage(SummaryGuid); + if (summaryPage == null) { + + summaryPage = new ProjectPage(project, typeof(Summary), false, SummaryGuid) { + name = "Summary", + }; + project.pages.Add(summaryPage); + } + + SetActivePage(summaryPage); + } + public bool KeyDown(SDL.SDL_Keysym key) { var ctrl = (key.mod & SDL.SDL_Keymod.KMOD_CTRL) != 0; if (ctrl) { diff --git a/YAFC/Windows/ProjectPageSettingsPanel.cs b/YAFC/Windows/ProjectPageSettingsPanel.cs index b9857881..dee124fe 100644 --- a/YAFC/Windows/ProjectPageSettingsPanel.cs +++ b/YAFC/Windows/ProjectPageSettingsPanel.cs @@ -68,14 +68,20 @@ public override void Build(ImGui gui) { gui.allocator = RectAllocator.LeftRow; if (editingPage != null && gui.BuildRedButton("Delete page")) { - Project.current.RemovePage(editingPage); + if (editingPage.canDelete) { + Project.current.RemovePage(editingPage); + } + else { + // Only hide if the (singleton) page cannot be deleted + MainScreen.Instance.ClosePage(editingPage.guid); + } Close(); } } } private void OtherToolsDropdown(ImGui gui) { - if (gui.BuildContextMenuButton("Duplicate page")) { + if (editingPage.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton("Duplicate page")) { gui.CloseDropdown(); var project = editingPage.owner; var collector = new ErrorCollector(); @@ -92,7 +98,7 @@ private void OtherToolsDropdown(ImGui gui) { } } - if (gui.BuildContextMenuButton("Share (export string to clipboard)")) { + if (editingPage.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton("Share (export string to clipboard)")) { gui.CloseDropdown(); var data = JsonUtils.SaveToJson(editingPage); using (var targetStream = new MemoryStream()) { diff --git a/YAFC/Workspace/ProductionTable/ProductionTableView.cs b/YAFC/Workspace/ProductionTable/ProductionTableView.cs index 41f47a69..69fb393a 100644 --- a/YAFC/Workspace/ProductionTable/ProductionTableView.cs +++ b/YAFC/Workspace/ProductionTable/ProductionTableView.cs @@ -712,9 +712,9 @@ private void DrawDesiredProduct(ImGui gui, ProductionLink element) { element.RecordUndo().amount = newAmount; } - public override void Rebuild(bool visuaOnly = false) { + public override void Rebuild(bool visualOnly = false) { flatHierarchyBuilder.SetData(model); - base.Rebuild(visuaOnly); + base.Rebuild(visualOnly); } private void BuildGoodsIcon(ImGui gui, Goods goods, ProductionLink link, float amount, ProductDropdownType dropdownType, RecipeRow recipe, ProductionTable context, Goods[] variants = null) { diff --git a/YAFC/Workspace/SummaryView.cs b/YAFC/Workspace/SummaryView.cs new file mode 100644 index 00000000..4abb34ee --- /dev/null +++ b/YAFC/Workspace/SummaryView.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using YAFC.Model; +using YAFC.UI; + +namespace YAFC { + public class SummaryView : ProjectPageView { + private class SummaryScrollArea : ScrollArea { + static float DefaultHeight = 10; + + public SummaryScrollArea(GuiBuilder builder) : base(DefaultHeight, builder, default, false, true, true) { + } + + public new void Build(ImGui gui) { + // Maximize scroll area to fit parent area (minus header and 'show issues' heights, and some (2) padding probably) + Build(gui, gui.valid ? gui.parent.contentSize.Y - HeaderFont.size - Font.text.size - ScrollbarSize - 2 : DefaultHeight); + } + } + + private class SummaryTabColumn : TextDataColumn { + private const float FirstColumnWidth = 14f; // About 20 'o' wide + + public SummaryTabColumn() : base("Tab", FirstColumnWidth) { + } + + public override void BuildElement(ImGui gui, ProjectPage page) { + if (page?.contentType != typeof(ProductionTable)) { + return; + } + + using (gui.EnterGroup(new Padding(0.5f, 0.2f, 0.2f, 0.5f))) { + gui.spacing = 0.2f; + if (page.icon != null) + gui.BuildIcon(page.icon.icon); + else gui.AllocateRect(0f, 1.5f); + gui.BuildText(page.name); + } + } + } + + private class SummaryDataColumn : TextDataColumn { + protected readonly SummaryView view; + + public SummaryDataColumn(SummaryView view) : base("Linked", float.MaxValue) { + this.view = view; + } + + public override void BuildElement(ImGui gui, ProjectPage page) { + if (page?.contentType != typeof(ProductionTable)) { + return; + } + + var table = page.content as ProductionTable; + using var grid = gui.EnterInlineGrid(ElementWidth, ElementSpacing); + foreach (KeyValuePair goodInfo in view.allGoods) { + if (!view.searchQuery.Match(goodInfo.Key)) { + continue; + } + + float amountAvailable = YAFCRounding((goodInfo.Value.totalProvided > 0 ? goodInfo.Value.totalProvided : 0) + goodInfo.Value.extraProduced); + float amountNeeded = YAFCRounding((goodInfo.Value.totalProvided < 0 ? -goodInfo.Value.totalProvided : 0) + goodInfo.Value.totalNeeded); + if (view.model.showOnlyIssues && (Math.Abs(amountAvailable - amountNeeded) < Epsilon || amountNeeded == 0)) { + continue; + } + + grid.Next(); + bool enoughProduced = amountAvailable >= amountNeeded; + ProductionLink link = table.links.Find(x => x.goods.name == goodInfo.Key); + if (link != null) { + if (link.amount != 0f) { + DrawProvideProduct(gui, link, page, goodInfo.Value, enoughProduced); + } + } + else { + if (Array.Exists(table.flow, x => x.goods.name == goodInfo.Key)) { + ProductionTableFlow flow = Array.Find(table.flow, x => x.goods.name == goodInfo.Key); + if (Math.Abs(flow.amount) > Epsilon) { + + DrawRequestProduct(gui, flow, enoughProduced); + } + } + } + } + } + + static private void DrawProvideProduct(ImGui gui, ProductionLink element, ProjectPage page, GoodDetails goodInfo, bool enoughProduced) { + gui.allocator = RectAllocator.Stretch; + gui.spacing = 0f; + + GoodsWithAmountEvent evt = gui.BuildFactorioObjectWithEditableAmount(element.goods, element.amount, element.goods.flowUnitOfMeasure, out float newAmount, (element.amount > 0 && enoughProduced) || (element.amount < 0 && goodInfo.extraProduced == -element.amount) ? SchemeColor.Primary : SchemeColor.Error); + if (evt == GoodsWithAmountEvent.TextEditing && newAmount != 0) { + SetProviderAmount(element, page, newAmount); + } + else if (evt == GoodsWithAmountEvent.ButtonClick) { + SetProviderAmount(element, page, YAFCRounding(goodInfo.sum)); + } + } + static private void DrawRequestProduct(ImGui gui, ProductionTableFlow flow, bool enoughProduced) { + gui.allocator = RectAllocator.Stretch; + gui.spacing = 0f; + gui.BuildFactorioObjectWithAmount(flow.goods, -flow.amount, flow.goods?.flowUnitOfMeasure ?? UnitOfMeasure.None, flow.amount > Epsilon ? enoughProduced ? SchemeColor.Green : SchemeColor.Error : SchemeColor.None); + } + + static private void SetProviderAmount(ProductionLink element, ProjectPage page, float newAmount) { + element.RecordUndo().amount = newAmount; + // Hack: Force recalculate the page (and make sure to catch the content change event caused by the recalculation) + page.SetActive(true); + page.SetToRecalculate(); + page.SetActive(false); + } + } + + static readonly float Epsilon = 1e-5f; + static readonly float ElementWidth = 3; + static readonly float ElementSpacing = 1; + struct GoodDetails { + public float totalProvided; + public float totalNeeded; + public float extraProduced; + public float sum; + } + + static Font HeaderFont = Font.header; + + private Project project; + private SearchQuery searchQuery; + + private readonly SummaryScrollArea scrollArea; + private readonly SummaryDataColumn goodsColumn; + private readonly DataGrid mainGrid; + + private readonly Dictionary allGoods = new Dictionary(); + + + public SummaryView() { + goodsColumn = new SummaryDataColumn(this); + var columns = new TextDataColumn[] + { + new SummaryTabColumn(), + goodsColumn, + }; + scrollArea = new SummaryScrollArea(BuildScrollArea); + mainGrid = new DataGrid(columns); + } + + public void SetProject(Project project) { + if (this.project != null) { + this.project.metaInfoChanged -= Recalculate; + foreach (ProjectPage page in project.pages) { + page.contentChanged -= Recalculate; + } + } + + this.project = project; + + project.metaInfoChanged += Recalculate; + foreach (ProjectPage page in project.pages) { + page.contentChanged += Recalculate; + } + } + + protected override void BuildPageTooltip(ImGui gui, Summary contents) { + } + + protected override void BuildHeader(ImGui gui) { + base.BuildHeader(gui); + + gui.allocator = RectAllocator.Center; + gui.BuildText("Production Sheet Summary", HeaderFont, false, RectAlignment.Middle); + gui.allocator = RectAllocator.LeftAlign; + } + + protected override void BuildContent(ImGui gui) { + if (gui.BuildCheckBox("Only show issues", model.showOnlyIssues, out bool newValue)) { + model.showOnlyIssues = newValue; + Recalculate(); + } + + scrollArea.Build(gui); + } + + private void BuildScrollArea(ImGui gui) { + foreach (Guid displayPage in project.displayPages) { + ProjectPage page = project.FindPage(displayPage); + if (page?.contentType != typeof(ProductionTable)) + continue; + + mainGrid.BuildRow(gui, page); + } + } + + private void Recalculate() => Recalculate(false); + + private void Recalculate(bool visualOnly) { + allGoods.Clear(); + foreach (Guid displayPage in project.displayPages) { + ProjectPage page = project.FindPage(displayPage); + ProductionTable content = page?.content as ProductionTable; + if (content == null) { + continue; + } + + foreach (ProductionLink link in content.links) { + if (link.amount != 0f) { + GoodDetails value = allGoods.GetValueOrDefault(link.goods.name); + value.totalProvided += YAFCRounding(link.amount); ; + allGoods[link.goods.name] = value; + } + } + + foreach (ProductionTableFlow flow in content.flow) { + if (flow.amount < -Epsilon) { + GoodDetails value = allGoods.GetValueOrDefault(flow.goods.name); + value.totalNeeded -= YAFCRounding(flow.amount); ; + value.sum -= YAFCRounding(flow.amount); ; + allGoods[flow.goods.name] = value; + } + else if (flow.amount > Epsilon) { + if (!content.links.Exists(x => x.goods == flow.goods)) { + // Only count extras if not linked + GoodDetails value = allGoods.GetValueOrDefault(flow.goods.name); + value.extraProduced += YAFCRounding(flow.amount); + value.sum -= YAFCRounding(flow.amount); + allGoods[flow.goods.name] = value; + } + } + } + } + + int count = 0; + foreach (KeyValuePair entry in allGoods) { + float amountAvailable = YAFCRounding((entry.Value.totalProvided > 0 ? entry.Value.totalProvided : 0) + entry.Value.extraProduced); + float amountNeeded = YAFCRounding((entry.Value.totalProvided < 0 ? -entry.Value.totalProvided : 0) + entry.Value.totalNeeded); + if (model != null && model.showOnlyIssues && (Math.Abs(amountAvailable - amountNeeded) < Epsilon || amountNeeded == 0)) { + continue; + } + count++; + } + + goodsColumn.width = count * (ElementWidth + ElementSpacing); + + Rebuild(visualOnly); + scrollArea.RebuildContents(); + } + + // Convert/truncate value as shown in UI to prevent slight mismatches + static private float YAFCRounding(float value) { +#pragma warning disable CA1806 // We don't care about the returned value as result is updated independently whether the function return true or not + DataUtils.TryParseAmount(DataUtils.FormatAmount(value, UnitOfMeasure.Second), out float result, UnitOfMeasure.Second); +#pragma warning restore CA1806 + + return result; + } + + public override void SetSearchQuery(SearchQuery query) { + searchQuery = query; + bodyContent.Rebuild(); + scrollArea.Rebuild(); + } + + public override void CreateModelDropdown(ImGui gui, Type type, Project project) { + } + } +} \ No newline at end of file diff --git a/YAFCmodel/Data/DataUtils.cs b/YAFCmodel/Data/DataUtils.cs index d74767d0..62cba571 100644 --- a/YAFCmodel/Data/DataUtils.cs +++ b/YAFCmodel/Data/DataUtils.cs @@ -375,11 +375,11 @@ public static string FormatTime(float time) { } public static string FormatAmount(float amount, UnitOfMeasure unit, string prefix = null, string suffix = null, bool precise = false) { - var (multplier, unitSuffix) = Project.current == null ? (1f, null) : Project.current.ResolveUnitOfMeasure(unit); - return FormatAmountRaw(amount, multplier, unitSuffix, prefix, suffix, precise ? PreciseFormat : FormatSpec); + var (multiplier, unitSuffix) = Project.current == null ? (1f, null) : Project.current.ResolveUnitOfMeasure(unit); + return FormatAmountRaw(amount, multiplier, unitSuffix, prefix, suffix, precise ? PreciseFormat : FormatSpec); } - public static string FormatAmountRaw(float amount, float unitMultipler, string unitSuffix, string prefix = null, string suffix = null, (char suffix, float multiplier, string format)[] formatSpec = null) { + public static string FormatAmountRaw(float amount, float unitMultiplier, string unitSuffix, string prefix = null, string suffix = null, (char suffix, float multiplier, string format)[] formatSpec = null) { if (float.IsNaN(amount) || float.IsInfinity(amount)) return "-"; if (amount == 0f) @@ -393,7 +393,7 @@ public static string FormatAmountRaw(float amount, float unitMultipler, string u amount = -amount; } - amount *= unitMultipler; + amount *= unitMultiplier; var idx = MathUtils.Clamp(MathUtils.Floor(MathF.Log10(amount)) + 8, 0, formatSpec.Length - 1); var val = formatSpec[idx]; amountBuilder.Append((amount * val.multiplier).ToString(val.format)); diff --git a/YAFCmodel/Model/ProjectPage.cs b/YAFCmodel/Model/ProjectPage.cs index 0d9cb53a..1c0ad896 100644 --- a/YAFCmodel/Model/ProjectPage.cs +++ b/YAFCmodel/Model/ProjectPage.cs @@ -13,16 +13,18 @@ public class ProjectPage : ModelObject { public bool visible { get; internal set; } [SkipSerialization] public string modelError { get; set; } public bool deleted { get; private set; } + public bool canDelete { get; } private uint lastSolvedVersion; private uint currentSolvingVersion; private uint actualVersion; public event Action contentChanged; - public ProjectPage(Project project, Type contentType, Guid guid = default) : base(project) { + public ProjectPage(Project project, Type contentType, bool canDelete = true, Guid guid = default) : base(project) { this.guid = guid == default ? Guid.NewGuid() : guid; actualVersion = project.projectVersion; this.contentType = contentType; + this.canDelete = canDelete; content = Activator.CreateInstance(contentType, this) as ProjectPageContents; } diff --git a/YAFCmodel/Model/Summary.cs b/YAFCmodel/Model/Summary.cs new file mode 100644 index 00000000..d40c7a33 --- /dev/null +++ b/YAFCmodel/Model/Summary.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace YAFC.Model { + public class Summary : ProjectPageContents { + + public bool showOnlyIssues { get; set; } + + public Summary(ModelObject page) : base(page) { } + + public override async Task Solve(ProjectPage page) { + return null; + } + } +} \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 4e43a3a9..062de5b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,6 +2,10 @@ Version: 0.6.2 Date: soon(tm) Changes: + - Add summary view + - Checkbox to show only goods with 'issues': different consuming/producing amounts + - Balance producing side to match the consuming when clicking an 'issue' + - Support the search box (ctrl+F) - Fix text alignment of about screen - Fix width of 'Target technology for cost analysis' preference popup ----------------------------------------------------------------------------------------------------------------------