diff --git a/YAFC/Widgets/ImmediateWidgets.cs b/YAFC/Widgets/ImmediateWidgets.cs index 631307e0..4c6118c0 100644 --- a/YAFC/Widgets/ImmediateWidgets.cs +++ b/YAFC/Widgets/ImmediateWidgets.cs @@ -150,7 +150,12 @@ public static void BuildInlineObjectListAndButton(this ImGui gui, ICollection } if (list.Count > count && gui.BuildButton("See full list") && gui.CloseDropdown()) { - SelectObjectPanel.Select(list, header, select, ordering, allowNone); + if (multiple) { + SelectMultiObjectPanel.Select(list, header, select, ordering, allowNone, checkMark); + } + else { + SelectSingleObjectPanel.Select(list, header, select, ordering, allowNone); + } } if (multiple && list.Count > 1) { diff --git a/YAFC/Widgets/PseudoScreen.cs b/YAFC/Widgets/PseudoScreen.cs index 351bb6a7..f7441856 100644 --- a/YAFC/Widgets/PseudoScreen.cs +++ b/YAFC/Widgets/PseudoScreen.cs @@ -68,10 +68,15 @@ public virtual bool KeyDown(SDL.SDL_Keysym key) { if (key.scancode == SDL.SDL_Scancode.SDL_SCANCODE_ESCAPE) { Close(false); } + if (key.scancode == SDL.SDL_Scancode.SDL_SCANCODE_RETURN || key.scancode == SDL.SDL_Scancode.SDL_SCANCODE_RETURN2 || key.scancode == SDL.SDL_Scancode.SDL_SCANCODE_KP_ENTER) { + ReturnPressed(); + } return true; } + protected virtual void ReturnPressed() { } + public virtual bool TextInput(string input) { return true; } diff --git a/YAFC/Windows/DependencyExplorer.cs b/YAFC/Windows/DependencyExplorer.cs index 13c6f7cd..4f5d1c92 100644 --- a/YAFC/Windows/DependencyExplorer.cs +++ b/YAFC/Windows/DependencyExplorer.cs @@ -111,7 +111,7 @@ public override void Build(ImGui gui) { using (gui.EnterRow()) { gui.BuildText("Currently inspecting:", Font.subheader); if (gui.BuildFactorioObjectButtonWithText(current)) { - SelectObjectPanel.Select(Database.objects.all, "Select something", Change); + SelectSingleObjectPanel.Select(Database.objects.all, "Select something", Change); } gui.BuildText("(Click to change)", color: SchemeColor.BackgroundTextFaint); diff --git a/YAFC/Windows/MainScreen.cs b/YAFC/Windows/MainScreen.cs index 35a0f1ce..2e7537c7 100644 --- a/YAFC/Windows/MainScreen.cs +++ b/YAFC/Windows/MainScreen.cs @@ -340,7 +340,7 @@ public void BuildSubHeader(ImGui gui, string text) { } private void ShowNeie() { - SelectObjectPanel.Select(Database.goods.all, "Open NEIE", NeverEnoughItemsPanel.Show); + SelectSingleObjectPanel.Select(Database.goods.all, "Open NEIE", NeverEnoughItemsPanel.Show); } private void SetSearch(SearchQuery searchQuery) { @@ -420,7 +420,7 @@ private void SettingsDropdown(ImGui gui) { } if (gui.BuildContextMenuButton("Dependency Explorer") && gui.CloseDropdown()) { - SelectObjectPanel.Select(Database.objects.all, "Open Dependency Explorer", DependencyExplorer.Show); + SelectSingleObjectPanel.Select(Database.objects.all, "Open Dependency Explorer", DependencyExplorer.Show); } BuildSubHeader(gui, "Extra"); diff --git a/YAFC/Windows/MessageBox.cs b/YAFC/Windows/MessageBox.cs index 67608c74..b37dc770 100644 --- a/YAFC/Windows/MessageBox.cs +++ b/YAFC/Windows/MessageBox.cs @@ -5,17 +5,12 @@ namespace YAFC { public class MessageBox : PseudoScreen { public MessageBox() : base(30f) { } - private static readonly MessageBox Instance = new MessageBox(); private string title, message, yes, no; public static void Show(Action result, string title, string message, string yes, string no) { - Instance.title = title; - Instance.complete = result; - Instance.message = message; - Instance.yes = yes; - Instance.no = no; - _ = MainScreen.Instance.ShowPseudoScreen(Instance); + MessageBox instance = new MessageBox { title = title, complete = result, message = message, yes = yes, no = no }; + _ = MainScreen.Instance.ShowPseudoScreen(instance); } public static void Show(string title, string message, string yes) { diff --git a/YAFC/Windows/MilestonesEditor.cs b/YAFC/Windows/MilestonesEditor.cs index c9ed805e..ce9973c7 100644 --- a/YAFC/Windows/MilestonesEditor.cs +++ b/YAFC/Windows/MilestonesEditor.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using YAFC.Model; using YAFC.UI; @@ -53,7 +54,7 @@ public override void Build(ImGui gui) { milestoneList.RebuildContents(); } if (gui.BuildButton("Add milestone")) { - SelectObjectPanel.Select(Database.objects.all, "Add new milestone", AddMilestone); + SelectMultiObjectPanel.Select(Database.objects.all.Except(Project.current.settings.milestones), "Add new milestone", AddMilestone); } } } diff --git a/YAFC/Windows/NeverEnoughItemsPanel.cs b/YAFC/Windows/NeverEnoughItemsPanel.cs index 7097a772..d2baa78e 100644 --- a/YAFC/Windows/NeverEnoughItemsPanel.cs +++ b/YAFC/Windows/NeverEnoughItemsPanel.cs @@ -343,7 +343,7 @@ public override void Build(ImGui gui) { } if (gui.BuildFactorioObjectButton(gui.lastRect, current, SchemeColor.Grey)) { - SelectObjectPanel.Select(Database.goods.all, "Select item", SetItem); + SelectSingleObjectPanel.Select(Database.goods.all, "Select item", SetItem); } using (var split = gui.EnterHorizontalSplit(2)) { diff --git a/YAFC/Windows/ProjectPageSettingsPanel.cs b/YAFC/Windows/ProjectPageSettingsPanel.cs index f6e10494..2b2c72d8 100644 --- a/YAFC/Windows/ProjectPageSettingsPanel.cs +++ b/YAFC/Windows/ProjectPageSettingsPanel.cs @@ -22,7 +22,7 @@ public class ProjectPageSettingsPanel : PseudoScreen { public static void Build(ImGui gui, ref string name, FactorioObject icon, Action setIcon) { _ = gui.BuildTextInput(name, out name, "Input name"); if (gui.BuildFactorioObjectButton(icon, 4f, MilestoneDisplay.None, SchemeColor.Grey)) { - SelectObjectPanel.Select(Database.objects.all, "Select icon", setIcon); + SelectSingleObjectPanel.Select(Database.objects.all, "Select icon", setIcon); } if (icon == null && gui.isBuilding) { diff --git a/YAFC/Windows/SelectMultiObjectPanel.cs b/YAFC/Windows/SelectMultiObjectPanel.cs new file mode 100644 index 00000000..4084cf03 --- /dev/null +++ b/YAFC/Windows/SelectMultiObjectPanel.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using YAFC.Model; +using YAFC.UI; + +namespace YAFC { + public class SelectMultiObjectPanel : SelectObjectPanel> { + private static readonly SelectMultiObjectPanel Instance = new SelectMultiObjectPanel(); + private readonly HashSet results = new HashSet(); + private bool allowAutoClose; + private Predicate checkMark; + + public SelectMultiObjectPanel() : base() { } + + public static void Select(IEnumerable list, string header, Action select, bool allowNone = false, Predicate checkMark = null) where T : FactorioObject { + Select(list, header, select, DataUtils.DefaultOrdering, allowNone, checkMark); + } + + public static void Select(IEnumerable list, string header, Action select, IComparer ordering, bool allowNone = false, Predicate checkMark = null) where T : FactorioObject { + Instance.allowAutoClose = true; + Instance.results.Clear(); + Instance.checkMark = (o) => checkMark?.Invoke((T)o) ?? false; // This is messy, but pushing T all the way around the call stack and type tree was messier. + Instance.Select(list, header, select, ordering, (xs, selectItem) => { + foreach (var x in xs ?? Enumerable.Empty()) { + selectItem(x); + } + }, allowNone); + } + + protected override void NonNullElementDrawer(ImGui gui, FactorioObject element, int index) { + bool click = gui.BuildFactorioObjectButton(element, display: MilestoneDisplay.Contained, bgColor: results.Contains(element) ? SchemeColor.Primary : SchemeColor.None, extendHeader: extendHeader); + + if (checkMark(element)) { + gui.DrawIcon(Rect.SideRect(gui.lastRect.TopLeft + new Vector2(1, 0), gui.lastRect.BottomRight - new Vector2(0, 1)), Icon.Check, SchemeColor.Green); + } + + if (click) { + if (!results.Add(element)) { + results.Remove(element); + } + if (!InputSystem.Instance.control && allowAutoClose) { + CloseWithResult(results); + } + allowAutoClose = false; + } + } + + public override void Build(ImGui gui) { + base.Build(gui); + using (gui.EnterGroup(default, RectAllocator.Center)) { + if (gui.BuildButton("OK")) { + CloseWithResult(results); + } + gui.BuildText("Hint: ctrl+click to select multiple", color: SchemeColor.BackgroundTextFaint); + } + } + + protected override void ReturnPressed() { + CloseWithResult(results); + } + } +} diff --git a/YAFC/Windows/SelectObjectPanel.cs b/YAFC/Windows/SelectObjectPanel.cs index 5ab6c918..19b0a3b2 100644 --- a/YAFC/Windows/SelectObjectPanel.cs +++ b/YAFC/Windows/SelectObjectPanel.cs @@ -6,64 +6,64 @@ using YAFC.UI; namespace YAFC { - public class SelectObjectPanel : PseudoScreen { - private static readonly SelectObjectPanel Instance = new SelectObjectPanel(); - private readonly SearchableList list; - private string header; - private Rect searchBox; - private bool extendHeader; - public SelectObjectPanel() : base(40f) { - list = new SearchableList(30, new Vector2(2.5f, 2.5f), ElementDrawer, ElementFilter); - } + public abstract class SelectObjectPanel : PseudoScreen { + protected readonly SearchableList list; + protected string header; + protected Rect searchBox; + protected bool extendHeader; - private bool ElementFilter(FactorioObject data, SearchQuery query) { - return data.Match(query); + protected SelectObjectPanel() : base(40f) { + list = new SearchableList(30, new Vector2(2.5f, 2.5f), ElementDrawer, ElementFilter); } - public static void Select(IEnumerable list, string header, Action select, IComparer ordering, bool allowNone) where T : FactorioObject { - _ = MainScreen.Instance.ShowPseudoScreen(Instance); - Instance.extendHeader = typeof(T) == typeof(FactorioObject); - List data = new List(list); + protected void Select(IEnumerable list, string header, Action select, IComparer ordering, Action> process, bool allowNone) where U : FactorioObject { + _ = MainScreen.Instance.ShowPseudoScreen(this); + extendHeader = typeof(U) == typeof(FactorioObject); + List data = new List(list); data.Sort(ordering); if (allowNone) { data.Insert(0, null); } - Instance.list.filter = default; - Instance.list.data = data; - Instance.header = header; - Instance.Rebuild(); - Instance.complete = (selected, x) => { - if (x is T t) { - if (ordering is DataUtils.FavoritesComparer favoritesComparer) { - favoritesComparer.AddToFavorite(t); + this.list.filter = default; + this.list.data = data; + this.header = header; + Rebuild(); + complete = (selected, x) => process(x, x => { + if (x is U u) { + if (ordering is DataUtils.FavoritesComparer favoritesComparer) { + favoritesComparer.AddToFavorite(u); } - select(t); + select(u); } else if (allowNone && selected) { select(null); } - }; + }); } - public static void Select(IEnumerable list, string header, Action select, bool allowNone = false) where T : FactorioObject { - Select(list, header, select, DataUtils.DefaultOrdering, allowNone); + protected void Select(IEnumerable list, string header, Action select, Action> process, bool allowNone = false) where U : FactorioObject { + Select(list, header, select, DataUtils.DefaultOrdering, process, allowNone); } private void ElementDrawer(ImGui gui, FactorioObject element, int index) { if (element == null) { if (gui.BuildRedButton(Icon.Close)) { - CloseWithResult(null); + CloseWithResult(default); } } else { - if (gui.BuildFactorioObjectButton(element, display: MilestoneDisplay.Contained, extendHeader: extendHeader)) { - CloseWithResult(element); - } + NonNullElementDrawer(gui, element, index); } } + protected abstract void NonNullElementDrawer(ImGui gui, FactorioObject element, int index); + + private bool ElementFilter(FactorioObject data, SearchQuery query) { + return data.Match(query); + } + public override void Build(ImGui gui) { BuildHeader(gui, header); if (gui.BuildSearchBox(list.filter, out var newFilter, "Start typing for search")) { diff --git a/YAFC/Windows/SelectSingleObjectPanel.cs b/YAFC/Windows/SelectSingleObjectPanel.cs new file mode 100644 index 00000000..108f20ed --- /dev/null +++ b/YAFC/Windows/SelectSingleObjectPanel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using YAFC.Model; +using YAFC.UI; + +namespace YAFC { + public class SelectSingleObjectPanel : SelectObjectPanel { + private static readonly SelectSingleObjectPanel Instance = new SelectSingleObjectPanel(); + public SelectSingleObjectPanel() : base() { } + + public static void Select(IEnumerable list, string header, Action select, bool allowNone = false) where T : FactorioObject { + Select(list, header, select, DataUtils.DefaultOrdering, allowNone); + } + + public static void Select(IEnumerable list, string header, Action select, IComparer ordering, bool allowNone = false) where T : FactorioObject { + Instance.Select(list, header, select, ordering, (x, selectItem) => selectItem(x), allowNone); + } + + protected override void NonNullElementDrawer(ImGui gui, FactorioObject element, int index) { + if (gui.BuildFactorioObjectButton(element, display: MilestoneDisplay.Contained, extendHeader: extendHeader)) { + CloseWithResult(element); + } + } + } +} diff --git a/YAFC/Workspace/AutoPlannerView.cs b/YAFC/Workspace/AutoPlannerView.cs index 061193bf..145ba4f0 100644 --- a/YAFC/Workspace/AutoPlannerView.cs +++ b/YAFC/Workspace/AutoPlannerView.cs @@ -45,7 +45,7 @@ void Page1(ImGui gui, ref bool valid) { } grid.Next(); if (gui.BuildButton(Icon.Plus, SchemeColor.Primary, SchemeColor.PrimaryAlt, size: 2.5f)) { - SelectObjectPanel.Select(Database.goods.all, "New production goal", x => { + SelectSingleObjectPanel.Select(Database.goods.all, "New production goal", x => { goal.Add(new AutoPlannerGoal { amount = 1f, item = x }); gui.Rebuild(); }); diff --git a/YAFC/Workspace/ProductionTable/ModuleCustomizationScreen.cs b/YAFC/Workspace/ProductionTable/ModuleCustomizationScreen.cs index 244abf3f..6ea7d462 100644 --- a/YAFC/Workspace/ProductionTable/ModuleCustomizationScreen.cs +++ b/YAFC/Workspace/ProductionTable/ModuleCustomizationScreen.cs @@ -31,7 +31,7 @@ public override void Build(ImGui gui) { if (template != null) { using (gui.EnterRow()) { if (gui.BuildFactorioObjectButton(template.icon)) { - SelectObjectPanel.Select(Database.objects.all, "Select icon", x => { + SelectSingleObjectPanel.Select(Database.objects.all, "Select icon", x => { template.RecordUndo().icon = x; Rebuild(); }); @@ -53,7 +53,7 @@ public override void Build(ImGui gui) { } grid.Next(); if (gui.BuildButton(Icon.Plus, SchemeColor.Primary, SchemeColor.PrimaryAlt, size: 1.5f)) { - SelectObjectPanel.Select(Database.allCrafters.Where(x => x.allowedEffects != AllowedEffects.None && !template.filterEntities.Contains(x)), "Add module template filter", sel => { + SelectSingleObjectPanel.Select(Database.allCrafters.Where(x => x.allowedEffects != AllowedEffects.None && !template.filterEntities.Contains(x)), "Add module template filter", sel => { template.RecordUndo().filterEntities.Add(sel); gui.Rebuild(); }); @@ -162,7 +162,7 @@ private void DrawRecipeModules(ImGui gui, EntityBeacon beacon, ref ModuleEffects grid.Next(); var evt = gui.BuildFactorioObjectWithEditableAmount(module.module, module.fixedCount, UnitOfMeasure.None, out float newAmount); if (evt == GoodsWithAmountEvent.ButtonClick) { - SelectObjectPanel.Select(GetModules(beacon), "Select module", sel => { + SelectSingleObjectPanel.Select(GetModules(beacon), "Select module", sel => { if (sel == null) { _ = modules.RecordUndo().list.Remove(module); } diff --git a/YAFC/Workspace/ProductionTable/ModuleFillerParametersScreen.cs b/YAFC/Workspace/ProductionTable/ModuleFillerParametersScreen.cs index bcb8ef03..e36e3564 100644 --- a/YAFC/Workspace/ProductionTable/ModuleFillerParametersScreen.cs +++ b/YAFC/Workspace/ProductionTable/ModuleFillerParametersScreen.cs @@ -46,13 +46,13 @@ public override void Build(ImGui gui) { gui.BuildText("Filler module:", Font.subheader); gui.BuildText("Use this module when aufofill doesn't add anything (for example when productivity modules doesn't fit)", wrap: true); if (gui.BuildFactorioObjectButtonWithText(modules.fillerModule)) { - SelectObjectPanel.Select(Database.allModules, "Select filler module", select => { modules.RecordUndo().fillerModule = select; }, true); + SelectSingleObjectPanel.Select(Database.allModules, "Select filler module", select => { modules.RecordUndo().fillerModule = select; }, true); } gui.AllocateSpacing(); gui.BuildText("Beacons & beacon modules:", Font.subheader); if (gui.BuildFactorioObjectButtonWithText(modules.beacon)) { - SelectObjectPanel.Select(Database.allBeacons, "Select beacon", select => { + SelectSingleObjectPanel.Select(Database.allBeacons, "Select beacon", select => { _ = modules.RecordUndo(); modules.beacon = select; if (modules.beaconModule != null && (modules.beacon == null || !modules.beacon.CanAcceptModule(modules.beaconModule.module))) { @@ -64,7 +64,7 @@ public override void Build(ImGui gui) { } if (gui.BuildFactorioObjectButtonWithText(modules.beaconModule)) { - SelectObjectPanel.Select(Database.allModules.Where(x => modules.beacon?.CanAcceptModule(x.module) ?? false), "Select module for beacon", select => { modules.RecordUndo().beaconModule = select; }, true); + SelectSingleObjectPanel.Select(Database.allModules.Where(x => modules.beacon?.CanAcceptModule(x.module) ?? false), "Select module for beacon", select => { modules.RecordUndo().beaconModule = select; }, true); } using (gui.EnterRow()) { diff --git a/YAFC/Workspace/ProductionTable/ProductionTableView.cs b/YAFC/Workspace/ProductionTable/ProductionTableView.cs index dde417b6..b0576df7 100644 --- a/YAFC/Workspace/ProductionTable/ProductionTableView.cs +++ b/YAFC/Workspace/ProductionTable/ProductionTableView.cs @@ -117,7 +117,7 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { } if (recipe.subgroup != null && imgui.BuildButton("Add raw recipe") && imgui.CloseDropdown()) { - SelectObjectPanel.Select(Database.recipes.all, "Select raw recipe", r => view.AddRecipe(recipe.subgroup, r)); + SelectMultiObjectPanel.Select(Database.recipes.all, "Select raw recipe", r => view.AddRecipe(recipe.subgroup, r), checkMark: r => recipe.subgroup.recipes.Any(rr => rr.recipe == r)); } if (recipe.subgroup != null && imgui.BuildButton("Unpack nested table")) { @@ -168,7 +168,7 @@ private void RemoveZeroRecipes(ProductionTable productionTable) { public override void BuildMenu(ImGui gui) { if (gui.BuildButton("Add recipe") && gui.CloseDropdown()) { - SelectObjectPanel.Select(Database.recipes.all, "Select raw recipe", r => view.AddRecipe(view.model, r)); + SelectMultiObjectPanel.Select(Database.recipes.all, "Select raw recipe", r => view.AddRecipe(view.model, r), checkMark: r => view.model.recipes.Any(rr => rr.recipe == r)); } gui.BuildText("Export inputs and outputs to blueprint with constant combinators:", wrap: true); @@ -366,7 +366,7 @@ private void ShowEntityDropdown(ImGui imgui, RecipeRow recipe) { public override void BuildMenu(ImGui gui) { if (gui.BuildButton("Mass set assembler") && gui.CloseDropdown()) { - SelectObjectPanel.Select(Database.allCrafters, "Set assembler for all recipes", set => { + SelectSingleObjectPanel.Select(Database.allCrafters, "Set assembler for all recipes", set => { DataUtils.FavoriteCrafter.AddToFavorite(set, 10); foreach (var recipe in view.GetRecipesRecursive()) { if (recipe.recipe.crafters.Contains(set)) { @@ -380,7 +380,7 @@ public override void BuildMenu(ImGui gui) { } if (gui.BuildButton("Mass set fuel") && gui.CloseDropdown()) { - SelectObjectPanel.Select(Database.goods.all.Where(x => x.fuelValue > 0), "Set fuel for all recipes", set => { + SelectSingleObjectPanel.Select(Database.goods.all.Where(x => x.fuelValue > 0), "Set fuel for all recipes", set => { DataUtils.FavoriteFuel.AddToFavorite(set, 10); foreach (var recipe in view.GetRecipesRecursive()) { if (recipe.entity != null && recipe.entity.energy.fuels.Contains(set)) { @@ -663,7 +663,7 @@ async void addRecipe(Recipe rec) { } } } - if (!allRecipes.Contains(rec) || (await MessageBox.Show("Recipe already exists", "Add a second copy?", "Add a copy", "Cancel")).choice) { + if (!allRecipes.Contains(rec) || (await MessageBox.Show("Recipe already exists", $"Add a second copy of {rec.locName}?", "Add a copy", "Cancel")).choice) { _ = AddRecipe(context, rec); } } @@ -1116,7 +1116,7 @@ protected override void BuildContent(ImGui gui) { } private void AddDesiredProductAtLevel(ProductionTable table) { - SelectObjectPanel.Select(Database.goods.all, "Add desired product", product => { + SelectMultiObjectPanel.Select(Database.goods.all.Except(table.linkMap.Where(p => p.Value.amount != 0).Select(p => p.Key)), "Add desired product", product => { if (table.linkMap.TryGetValue(product, out var existing)) { if (existing.amount != 0) { return; diff --git a/changelog.txt b/changelog.txt index 3ebaafcc..a0da80c9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,7 +2,7 @@ Version: 0.6.3 Date: TBA Changes: - - + - Allow selecting multiple object with CTRL-click in places it makes sense. ---------------------------------------------------------------------------------------------------------------------- Version: 0.6.2 Date: March 2024