Skip to content

Commit

Permalink
Add Ctrl+click recipe hints (shpaass#220)
Browse files Browse the repository at this point in the history
This adds hints for adding recipes via ctrl+click. They will adapt to
the situation, to either tell the user what will happen:

![image](https://github.com/user-attachments/assets/068bcb37-5f92-45ca-b3aa-fdd71f0a1235)


![image](https://github.com/user-attachments/assets/c00e495d-d1be-446b-8d79-e5739586be6b)

or to explain how to make ctrl+clicking work:

![image](https://github.com/user-attachments/assets/99495863-2495-41e2-a0d1-f9e3e915fc54)

The tips only appear in the appropriate section (Made with or Needed
for) and only when recipes can be added.
  • Loading branch information
shpaass authored Aug 1, 2024
2 parents 439f227 + 8dbcba3 commit f67551a
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 70 deletions.
81 changes: 56 additions & 25 deletions Yafc.Model/Data/DataUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,37 +96,68 @@ public static Bits GetMilestoneOrder(FactorioId id) {

public static readonly Random random = new Random();

public static bool SelectSingle<T>(this T[] list, [NotNullWhen(true)] out T? element) where T : FactorioObject {
var userFavorites = Project.current.preferences.favorites;
bool acceptOnlyFavorites = false;
element = null;
foreach (var elem in list) {
if (!elem.IsAccessibleWithCurrentMilestones() || elem.specialType != FactorioObjectSpecialType.Normal) {
continue;
/// <summary>
/// Call to get the favorite or only useful item in the list, considering milestones, accessibility, and <see cref="FactorioObject.specialType"/>, provided there is exactly one such item.
/// If no best item exists, returns <see langword="null"/>. Always returns a tooltip applicable to using ctrl+click to add a recipe.
/// </summary>
/// <typeparam name="T">The element type of <paramref name="list"/>. This type must be derived from <see cref="FactorioObject"/>.</typeparam>
/// <param name="list">The array of items to search.</param>
/// <param name="recipeHint">Upon return, contains a hint that is applicable to using ctrl+click to add a recipe.
/// This will either suggest using ctrl+click, or explain why ctrl+click cannot be used.
/// It is not useful when <typeparamref name="T"/> is not <see cref="Recipe"/>.</param>
/// <returns>Items that are not accessible at the current milestones are always ignored. After those have been discarded, the return value is the first applicable entry in the following list:
/// <list type="bullet">
/// <item>The only normal item in <paramref name="list"/>.</item>
/// <item>The only normal user favorite in <paramref name="list"/>.</item>
/// <item>The only item in <paramref name="list"/>, considering both normal and special items.</item>
/// <item>The only user favorite in <paramref name="list"/>, considering both normal and special items.</item>
/// <item>If no previous options are applicable, <see langword="null"/>.</item>
/// </list></returns>
public static T? SelectSingle<T>(this T[] list, out string recipeHint) where T : FactorioObject {
return @internal(list, true, out recipeHint) ?? @internal(list, false, out recipeHint);

static T? @internal(T[] list, bool excludeSpecial, out string recipeHint) {
HashSet<FactorioObject> userFavorites = Project.current.preferences.favorites;
bool acceptOnlyFavorites = false;
T? element = null;
if (list.Any(t => t.IsAccessible())) {
recipeHint = "Hint: Complete milestones to enable ctrl+click";
}

if (userFavorites.Contains(elem)) {
if (!acceptOnlyFavorites || element == null) {
element = elem;
acceptOnlyFavorites = true;
}
else {
element = null;
return false;
}
else {
recipeHint = "Hint: Mark a recipe as accessible to enable ctrl+click";
}
else if (!acceptOnlyFavorites) {
if (element == null) {
element = elem;
foreach (T elem in list) {
// Always consider normal entries. A list with two normals and one special should select nothing, rather than selecting the only special item.
if (!elem.IsAccessibleWithCurrentMilestones() || (elem.specialType != FactorioObjectSpecialType.Normal && excludeSpecial)) {
continue;
}
else {
element = null;
acceptOnlyFavorites = true;

if (userFavorites.Contains(elem)) {
if (!acceptOnlyFavorites || element == null) {
element = elem;
recipeHint = "Hint: ctrl+click to add your favorited recipe";
acceptOnlyFavorites = true;
}
else {
recipeHint = "Hint: Cannot ctrl+click with multiple favorited recipes";
return null;
}
}
else if (!acceptOnlyFavorites) {
if (element == null) {
element = elem;
recipeHint = excludeSpecial ? "Hint: ctrl+click to add the accessible normal recipe" : "Hint: ctrl+click to add the accessible recipe";
}
else {
element = null;
recipeHint = "Hint: Set a favorite recipe to add it with ctrl+click";
acceptOnlyFavorites = true;
}
}
}
}

return element != null;
return element;
}
}

public static void SetupForProject(Project project) {
Expand Down
16 changes: 8 additions & 8 deletions Yafc/Widgets/ImmediateWidgets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public static bool BuildFloatInput(this ImGui gui, float value, out float newVal
return false;
}

public static Click BuildFactorioObjectButton(this ImGui gui, Rect rect, FactorioObject? obj, SchemeColor bgColor = SchemeColor.None, bool extendHeader = false) {
public static Click BuildFactorioObjectButton(this ImGui gui, Rect rect, FactorioObject? obj, SchemeColor bgColor = SchemeColor.None, ObjectTooltipOptions tooltipOptions = default) {
SchemeColor overColor;
if (bgColor == SchemeColor.None) {
overColor = SchemeColor.Grey;
Expand All @@ -82,7 +82,7 @@ public static Click BuildFactorioObjectButton(this ImGui gui, Rect rect, Factori
}
var evt = gui.BuildButton(rect, bgColor, overColor, button: 0);
if (evt == ButtonEvent.MouseOver && obj != null) {
MainScreen.Instance.ShowTooltip(obj, gui, rect, extendHeader);
MainScreen.Instance.ShowTooltip(obj, gui, rect, tooltipOptions);
}
else if (evt == ButtonEvent.Click) {
if (gui.actionParameter == SDL.SDL_BUTTON_MIDDLE && obj != null) {
Expand All @@ -109,9 +109,9 @@ public static Click BuildFactorioObjectButton(this ImGui gui, Rect rect, Factori
/// <summary>Draws a button displaying the icon belonging to a <see cref="FactorioObject"/>, or an empty box as a placeholder if no object is available.</summary>
/// <param name="obj">Draw the icon for this object, or an empty box if this is <see langword="null"/>.</param>
/// <param name="useScale">If <see langword="true"/>, this icon will be displayed at <see cref="ProjectPreferences.iconScale"/>, instead of at 100% scale.</param>
public static Click BuildFactorioObjectButton(this ImGui gui, FactorioObject? obj, float size = 2f, MilestoneDisplay display = MilestoneDisplay.Normal, SchemeColor bgColor = SchemeColor.None, bool extendHeader = false, bool useScale = false) {
public static Click BuildFactorioObjectButton(this ImGui gui, FactorioObject? obj, float size = 2f, MilestoneDisplay display = MilestoneDisplay.Normal, SchemeColor bgColor = SchemeColor.None, bool useScale = false, ObjectTooltipOptions tooltipOptions = default) {
gui.BuildFactorioObjectIcon(obj, display, size, useScale);
return gui.BuildFactorioObjectButton(gui.lastRect, obj, bgColor, extendHeader);
return gui.BuildFactorioObjectButton(gui.lastRect, obj, bgColor, tooltipOptions);
}

public static Click BuildFactorioObjectButtonWithText(this ImGui gui, FactorioObject? obj, string? extraText = null, float size = 2f, MilestoneDisplay display = MilestoneDisplay.Normal) {
Expand Down Expand Up @@ -205,11 +205,11 @@ public static void BuildInlineObjectListAndButtonWithNone<T>(this ImGui gui, ICo
/// <param name="amount">Display this value, formatted appropriately for <paramref name="unit"/>.</param>
/// <param name="unit">Use this unit of measure when formatting <paramref name="amount"/> for display.</param>
/// <param name="useScale">If <see langword="true"/>, this icon will be displayed at <see cref="ProjectPreferences.iconScale"/>, instead of at 100% scale.</param>
public static Click BuildFactorioObjectWithAmount(this ImGui gui, FactorioObject? goods, float amount, UnitOfMeasure unit, SchemeColor bgColor = SchemeColor.None, SchemeColor textColor = SchemeColor.None, bool useScale = true) {
public static Click BuildFactorioObjectWithAmount(this ImGui gui, FactorioObject? goods, float amount, UnitOfMeasure unit, SchemeColor bgColor = SchemeColor.None, SchemeColor textColor = SchemeColor.None, bool useScale = true, ObjectTooltipOptions tooltipOptions = default) {
using (gui.EnterFixedPositioning(3f, 3f, default)) {
gui.allocator = RectAllocator.Stretch;
gui.spacing = 0f;
Click clicked = gui.BuildFactorioObjectButton(goods, 3f, MilestoneDisplay.Contained, bgColor, useScale: useScale);
Click clicked = gui.BuildFactorioObjectButton(goods, 3f, MilestoneDisplay.Contained, bgColor, useScale, tooltipOptions);
if (goods != null) {
gui.BuildText(DataUtils.FormatAmount(amount, unit), Font.text, false, RectAlignment.Middle, textColor);
if (InputSystem.Instance.control && gui.BuildButton(gui.lastRect, SchemeColor.None, SchemeColor.Grey) == ButtonEvent.MouseOver) {
Expand Down Expand Up @@ -266,11 +266,11 @@ public static void BuildObjectSelectDropDownWithNone<T>(this ImGui gui, ICollect
/// <param name="newAmount">The new value entered by the user, if this returns <see cref="GoodsWithAmountEvent.TextEditing"/>. Otherwise, the original <paramref name="amount"/>.</param>
/// <param name="allowScroll">If <see langword="true"/>, the default, the user can adjust the value by using the scroll wheel while hovering over the editable text.
/// If <see langword="false"/>, the scroll wheel will be ignored when hovering.</param>
public static GoodsWithAmountEvent BuildFactorioObjectWithEditableAmount(this ImGui gui, FactorioObject? obj, float amount, UnitOfMeasure unit, out float newAmount, SchemeColor color = SchemeColor.None, bool useScale = true, bool allowScroll = true) {
public static GoodsWithAmountEvent BuildFactorioObjectWithEditableAmount(this ImGui gui, FactorioObject? obj, float amount, UnitOfMeasure unit, out float newAmount, SchemeColor color = SchemeColor.None, bool useScale = true, bool allowScroll = true, ObjectTooltipOptions tooltipOptions = default) {
using var group = gui.EnterGroup(default, RectAllocator.Stretch, spacing: 0f);
group.SetWidth(3f);
newAmount = amount;
GoodsWithAmountEvent evt = (GoodsWithAmountEvent)gui.BuildFactorioObjectButton(obj, 3f, MilestoneDisplay.Contained, color);
GoodsWithAmountEvent evt = (GoodsWithAmountEvent)gui.BuildFactorioObjectButton(obj, 3f, MilestoneDisplay.Contained, color, useScale, tooltipOptions);

if (gui.BuildTextInput(DataUtils.FormatAmount(amount, unit), out string newText, null, Icon.None, true, default, RectAlignment.Middle, SchemeColor.Secondary)) {
if (DataUtils.TryParseAmount(newText, out newAmount, unit)) {
Expand Down
55 changes: 48 additions & 7 deletions Yafc/Widgets/ObjectTooltip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,39 @@
using Yafc.UI;

namespace Yafc {
/// <summary>
/// The location(s) where <see cref="ObjectTooltip"/> should display hints
/// (currently only "ctrl+click to add recipe" hints)
/// </summary>
[Flags]
public enum HintLocations {
/// <summary>
/// Do not display any hints.
/// </summary>
None = 0,
/// <summary>
/// Display the ctrl+click recipe-selection hint associated with recipes that produce this <see cref="Goods"/>.
/// </summary>
OnProducingRecipes = 1,
/// <summary>
/// Display the ctrl+click recipe-selection hint associated with recipes that consume this <see cref="Goods"/>.
/// </summary>
OnConsumingRecipes = 2,
// NOTE: This is [Flags]. The next item, if applicable, should be 4.
}

public class ObjectTooltip : Tooltip {
public static readonly Padding contentPadding = new Padding(1f, 0.25f);

public ObjectTooltip() : base(new Padding(0f, 0f, 0f, 0.5f), 25f) { }

private IFactorioObjectWrapper target = null!; // null-forgiving: Set by SetFocus, aka ShowTooltip.
/// <summary>
/// If <see langword="true"/> and the target object is not a <see cref="Goods"/>, this tooltip will specify the type of object.
/// </summary>
private bool extendHeader;
private ObjectTooltipOptions tooltipOptions;

private void BuildHeader(ImGui gui) {
using (gui.EnterGroup(new Padding(1f, 0.5f), RectAllocator.LeftAlign, spacing: 0f)) {
string name = target.text;
if (extendHeader && target is not Goods) {
if (tooltipOptions.ExtendHeader && target is not Goods) {
name = name + " (" + target.target.type + ")";
}

Expand Down Expand Up @@ -283,6 +301,10 @@ private void BuildGoods(Goods goods, ImGui gui) {
BuildSubHeader(gui, "Made with");
using (gui.EnterGroup(contentPadding)) {
BuildIconRow(gui, goods.production, 2);
if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnProducingRecipes)) {
goods.production.SelectSingle(out string recipeTip);
gui.BuildText(recipeTip, color: SchemeColor.BackgroundTextFaint);
}
}
}

Expand All @@ -297,6 +319,10 @@ private void BuildGoods(Goods goods, ImGui gui) {
BuildSubHeader(gui, "Needed for");
using (gui.EnterGroup(contentPadding)) {
BuildIconRow(gui, goods.usages, 4);
if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnConsumingRecipes)) {
goods.usages.SelectSingle(out string recipeTip);
gui.BuildText(recipeTip, color: SchemeColor.BackgroundTextFaint);
}
}
}

Expand Down Expand Up @@ -500,12 +526,27 @@ private void BuildTechnology(Technology technology, ImGui gui) {
}
}

public void SetFocus(IFactorioObjectWrapper target, ImGui gui, Rect rect, bool extendHeader = false) {
this.extendHeader = extendHeader;
public void SetFocus(IFactorioObjectWrapper target, ImGui gui, Rect rect, ObjectTooltipOptions tooltipOptions) {
this.tooltipOptions = tooltipOptions;
this.target = target;
base.SetFocus(gui, rect);
}

public bool IsSameObjectHovered(ImGui gui, FactorioObject? factorioObject) => source == gui && factorioObject == target.target && gui.IsMouseOver(sourceRect);
}

public struct ObjectTooltipOptions {
/// <summary>
/// If <see langword="true"/> and the target object is not a <see cref="Goods"/>, this tooltip will specify the type of object.
/// e.g. "Radar" is the item, "Radar (Recipe)" is the recipe, and "Radar (Entity)" is the building.
/// </summary>
public bool ExtendHeader { get; set; }
/// <summary>
/// Gets or sets flags indicating where hints should be displayed in the tooltip.
/// </summary>
public HintLocations HintLocations { get; set; }

// Reduce boilerplate by permitting unambiguous and relatively obvious implicit conversions.
public static implicit operator ObjectTooltipOptions(HintLocations hintLocations) => new() { HintLocations = hintLocations };
}
}
2 changes: 1 addition & 1 deletion Yafc/Windows/DependencyExplorer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private void DrawFactorioObject(ImGui gui, FactorioId id) {
string text = fobj.locName + " (" + fobj.type + ")";
gui.RemainingRow(0.5f).BuildText(text, null, true, color: fobj.IsAccessible() ? SchemeColor.BackgroundText : SchemeColor.BackgroundTextFaint);
}
if (gui.BuildFactorioObjectButton(gui.lastRect, fobj, extendHeader: true) == Click.Left) {
if (gui.BuildFactorioObjectButton(gui.lastRect, fobj, tooltipOptions: new() { ExtendHeader = true }) == Click.Left) {
Change(fobj);
}
}
Expand Down
4 changes: 2 additions & 2 deletions Yafc/Windows/MainScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,8 @@ private async void DoCheckForUpdates() {
}
}

public void ShowTooltip(IFactorioObjectWrapper obj, ImGui source, Rect sourceRect, bool extendHeader = false) {
objectTooltip.SetFocus(obj, source, sourceRect, extendHeader);
public void ShowTooltip(IFactorioObjectWrapper obj, ImGui source, Rect sourceRect, ObjectTooltipOptions tooltipOptions = default) {
objectTooltip.SetFocus(obj, source, sourceRect, tooltipOptions);
ShowTooltip(objectTooltip);
}

Expand Down
4 changes: 2 additions & 2 deletions Yafc/Windows/SelectMultiObjectPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public static void Select<T>(IEnumerable<T> list, string header, Action<T> selec
}, false);
}

protected override void NonNullElementDrawer(ImGui gui, FactorioObject element, int index) {
Click click = gui.BuildFactorioObjectButton(element, 2.5f, MilestoneDisplay.Contained, results.Contains(element) ? SchemeColor.Primary : SchemeColor.None, extendHeader, true);
protected override void NonNullElementDrawer(ImGui gui, FactorioObject element) {
Click click = gui.BuildFactorioObjectButton(element, 2.5f, MilestoneDisplay.Contained, results.Contains(element) ? SchemeColor.Primary : SchemeColor.None, true, new() { 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);
Expand Down
6 changes: 3 additions & 3 deletions Yafc/Windows/SelectObjectPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public abstract class SelectObjectPanel<T> : PseudoScreen<T> {
private string? noneTooltip;
/// <summary>
/// If <see langword="true"/> and the object being hovered is not a <see cref="Goods"/>, the <see cref="ObjectTooltip"/> should specify the type of object.
/// See also <see cref="ObjectTooltip.extendHeader"/>.
/// See also <see cref="ObjectTooltipOptions.ExtendHeader"/>.
/// </summary>
protected bool extendHeader { get; private set; }

Expand Down Expand Up @@ -80,15 +80,15 @@ private void ElementDrawer(ImGui gui, FactorioObject? element, int index) {
}
}
else {
NonNullElementDrawer(gui, element, index);
NonNullElementDrawer(gui, element);
}
}

/// <summary>
/// Called to draw a <see cref="FactorioObject"/> that should be displayed in this panel, and to handle mouse-over and click events.
/// <paramref name="element"/> will not be null. If a "none" or "clear" option is present, <see cref="SelectObjectPanel{T}"/> takes care of that option.
/// </summary>
protected abstract void NonNullElementDrawer(ImGui gui, FactorioObject element, int index);
protected abstract void NonNullElementDrawer(ImGui gui, FactorioObject element);

private bool ElementFilter(FactorioObject? data, SearchQuery query) => data?.Match(query) ?? true;

Expand Down
Loading

0 comments on commit f67551a

Please sign in to comment.