Skip to content

Commit

Permalink
Move and slightly improve ConfirmDialog #683
Browse files Browse the repository at this point in the history
This moves the ConfirmDialog out of the Drawable directories, since it
is used in more situations than Drawable-specific menus.

Additionally, some minor refactorings and improvements were made,
notably:
* The dialog is now more configurable. Specifically, the title,
description,
  button texts, button icons, and button colors can all be set to custom
  values.
  There is also a pre-made configuration for delete confirmations.
* The visuals of the dialog have been slightly changed. For example, the
 confirm button is now green by default, the dialog is slightly larger,
  there are icons on the buttons, and so on.
* The dialog should now be able to handle differing text lengths better.
* A static convenience method has been added to make it easier to
  ask the user confirmation questions.
  It suffices to call this static method on the class with the desired
  configuration (i.e., message etc.) and then await the result (see
  below), without the need for the caller to have access to a fitting
  GameObject for the dialog (or needing to handle destroying it).
* The dialog has been turned into a `PlatformDependentComponent`.
* A fade-in and fade-out animation has been implemented for the dialog.
* Instead of utilizing callbacks, the method to show the dialog now
  leverages its asynchronous nature by simply waiting until the user
  made their choice, then returning a boolean indicating that choice.
  This should also more easily enable patterns such as early return, as
  is commonly done with the analogous `window.confirm()` method in Web
  development.
  • Loading branch information
falko17 committed Nov 15, 2024
1 parent 13aaca7 commit 1865e76
Show file tree
Hide file tree
Showing 9 changed files with 746 additions and 208 deletions.

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions Assets/SEE/Controls/Actions/ContextMenuAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
using SEE.Game.City;
using SEE.Utils.History;
using SEE.GO.Menu;
using SEE.UI.Menu.Drawable;
using SEE.UI.Menu;
using SEE.UI.Window.PropertyWindow;
using SEE.XR;

Expand Down Expand Up @@ -358,7 +358,7 @@ private static IEnumerable<PopupMenuEntry> GetCommonOptions(PopupMenu popupMenu,
entries.Add(new PopupMenuHeading("Source: " + source, Priority: int.MaxValue));
entries.Add(new PopupMenuHeading("Target: " + target, Priority: int.MaxValue));
}
entries.Add(new PopupMenuAction("Delete", DeleteElement, Icons.Trash, Priority: 0));
entries.Add(new PopupMenuAction("Delete", () => DeleteElement().Forget(), Icons.Trash, Priority: 0));

entries.Add(new PopupMenuActionDoubleIcon("Inspect", () =>
{
Expand Down Expand Up @@ -397,7 +397,7 @@ private static IEnumerable<PopupMenuEntry> GetCommonOptions(PopupMenu popupMenu,

return entries;

void DeleteElement()
async UniTaskVoid DeleteElement()
{
if (graphElement is Node node && node.IsRoot())
{
Expand All @@ -414,8 +414,11 @@ void DeleteElement()
}
else
{
ConfirmDialogMenu confirm = new($"Do you really want to delete the element {graphElement.ID}?\r\nThis action cannot be undone.");
confirm.ExecuteAfterConfirmAsync(() => graphElement.ItsGraph.RemoveElement(graphElement)).Forget();
string message = $"Do you really want to delete the element {graphElement.ID}?\nThis action cannot be undone.";
if (await ConfirmDialog.ConfirmAsync(ConfirmConfiguration.Delete(message)))
{
graphElement.ItsGraph.RemoveElement(graphElement);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public static void Run()
/// </summary>
public static void OpenDebugAdapterConfig()
{
GameObject go = new GameObject("Debug Adapter Configuration");
GameObject go = new("Debug Adapter Configuration");

// create property group
PropertyGroup group = go.gameObject.AddComponent<PropertyGroup>();
Expand Down Expand Up @@ -224,7 +224,7 @@ void UpdateValues()
/// </summary>
public static void OpenLaunchConfig()
{
GameObject go = new GameObject("Launch Request");
GameObject go = new("Launch Request");

// create property group
PropertyGroup group = go.AddComponent<PropertyGroup>();
Expand Down
222 changes: 222 additions & 0 deletions Assets/SEE/UI/Menu/ConfirmDialog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using Michsky.UI.ModernUIPack;
using SEE.GO;
using SEE.Utils;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

namespace SEE.UI.Menu
{
/// <summary>
/// Configuration for the confirm dialog.
/// </summary>
/// <param name="description">The description of the dialog, can be up to three lines long.</param>
/// <param name="title">The title of the dialog displayed in the top bar. Should be kept short.</param>
/// <param name="yesText">The text of the confirm button.</param>
/// <param name="noText">The text of the cancel button.</param>
/// <param name="yesIcon">The icon of the confirm button, given as a FontAwesome character (see <see cref="Icons"/>).</param>
/// <param name="noIcon">The icon of the cancel button, given as a FontAwesome character (see <see cref="Icons"/>).</param>
/// <param name="yesColor">The color of the confirm button. Note that the text will be white, so choose a dark color.</param>
public record ConfirmConfiguration(
string description,
string title = "Are you sure?",
string yesText = "Confirm",
string noText = "Cancel",
char yesIcon = Icons.Checkmark,
char noIcon = 'X',
Color? yesColor = null
)
{
/// <summary>
/// A pre-made configuration for a delete dialog.
/// </summary>
/// <param name="description">The description of the dialog, can be up to three lines long.</param>
/// <returns>A configuration for a delete dialog.</returns>
public static ConfirmConfiguration Delete(string description)
{
return new ConfirmConfiguration(description, title: "Really delete?",
yesText: "Delete", yesIcon: Icons.Trash, yesColor: Color.red.Darker());
}
}

/// <summary>
/// A simple configurable confirmation dialog with a "confirm" and a "cancel" button.
/// </summary>
public class ConfirmDialog : PlatformDependentComponent
{
/// <summary>
/// The duration of the fade animation in seconds.
/// </summary>
private const float AnimationDuration = 1.0f;

/// <summary>
/// Prefab for the dialog.
/// </summary>
private static string DialogMenuPrefab => UIPrefabFolder + "ConfirmDialog";

/// <summary>
/// The default color of the confirm button.
/// </summary>
private static readonly Color DefaultColor = Color.green.Darker(); // Slightly darker green.

/// <summary>
/// Whether the dialog should be destroyed after it is closed (and faded out).
/// </summary>
private bool OneTime;

/// <summary>
/// The dialog menu game object.
/// </summary>
private GameObject Dialog { get; set; }

/// <summary>
/// The canvas group of the dialog, used for fading in and out.
/// </summary>
private CanvasGroup CanvasGroup { get; set; }

/// <summary>
/// The title of the dialog.
/// </summary>
private TextMeshProUGUI Title { get; set; }

/// <summary>
/// The description of the dialog.
/// Can be up to three lines long.
/// </summary>
private TextMeshProUGUI Description { get; set; }

/// <summary>
/// The negative/"cancel" button of the dialog.
/// </summary>
private ButtonManagerBasic NoButton { get; set; }

/// <summary>
/// The positive/"confirm" button of the dialog.
/// </summary>
private ButtonManagerBasic YesButton { get; set; }

/// <summary>
/// The image component of the <see cref="YesButton"/> controlling its color.
/// </summary>
private Image YesButtonImage { get; set; }

/// <summary>
/// The TextMeshPro component for the icon of the <see cref="NoButton"/>.
/// </summary>
private TextMeshProUGUI NoIcon { get; set; }

/// <summary>
/// The TextMeshPro component for the icon of the <see cref="YesButton"/>.
/// </summary>
private TextMeshProUGUI YesIcon { get; set; }

/// <summary>
/// An event that is invoked when the user makes a choice in the dialog (including closing it).
/// </summary>
private UnityEvent<bool> OnChoiceMade { get; } = new();

/// <summary>
/// The game object under which the dialogs are instantiated.
/// </summary>
private static readonly Lazy<GameObject> dialogGameObject = new(() => new("ConfirmDialogs"));

/// <summary>
/// The ongoing fade animation of the dialog, if any.
/// </summary>
private Tweener ExistingFade;

/// <summary>
/// Initializes the dialog.
/// </summary>
protected override void StartDesktop()
{
Dialog = PrefabInstantiator.InstantiatePrefab(DialogMenuPrefab, Canvas.transform, false);
CanvasGroup = Dialog.MustGetComponent<CanvasGroup>();
NoButton = Dialog.transform.Find("Content/Buttons/Cancel").gameObject.MustGetComponent<ButtonManagerBasic>();
NoIcon = NoButton.transform.Find("Texts/Icon").gameObject.MustGetComponent<TextMeshProUGUI>();
YesButton = Dialog.transform.Find("Content/Buttons/Confirm").gameObject.MustGetComponent<ButtonManagerBasic>();
YesIcon = YesButton.transform.Find("Texts/Icon").gameObject.MustGetComponent<TextMeshProUGUI>();
YesButtonImage = YesButton.gameObject.MustGetComponent<Image>();
Title = Dialog.transform.Find("Dragger/Text").gameObject.MustGetComponent<TextMeshProUGUI>();
Description = Dialog.transform.Find("Content/Description").gameObject.MustGetComponent<TextMeshProUGUI>();
ButtonManagerBasic CloseButton = Dialog.transform.Find("Dragger/CancelDragger").gameObject.MustGetComponent<ButtonManagerBasic>();

YesButton.clickEvent.AddListener(() => OnChoiceMade.Invoke(true));
NoButton.clickEvent.AddListener(() => OnChoiceMade.Invoke(false));
CloseButton.clickEvent.AddListener(() => OnChoiceMade.Invoke(false));
OnChoiceMade.AddListener(_ => CloseMenu());
}

private void OnDestroy()
{
if (Dialog != null)
{
Destroyer.Destroy(Dialog);
}
}

/// <summary>
/// Sets up the dialog with the given <paramref name="configuration"/> and fades it in.
/// </summary>
/// <param name="configuration">The configuration for the dialog.</param>
private void ShowMenu(ConfirmConfiguration configuration)
{
Title.text = configuration.title;
Description.text = configuration.description;
YesButton.buttonText = configuration.yesText;
NoButton.buttonText = configuration.noText;
YesIcon.text = configuration.yesIcon.ToString();
YesButtonImage.color = configuration.yesColor ?? DefaultColor;
NoIcon.text = configuration.noIcon.ToString();

Dialog.SetActive(true);
Dialog.transform.SetAsLastSibling();
ExistingFade?.Kill();
ExistingFade = CanvasGroup.DOFade(1f, AnimationDuration);
}

/// <summary>
/// Closes the dialog by fading it out.
/// </summary>
private void CloseMenu()
{
ExistingFade?.Kill();
ExistingFade = CanvasGroup.DOFade(0f, AnimationDuration).OnComplete(DisableMenu);
}

/// <summary>
/// Disables/destroys the dialog after it has been faded out.
/// </summary>
private void DisableMenu()
{
if (OneTime)
{
Destroyer.Destroy(this);
}
else
{
Dialog.SetActive(false);
}
}

/// <summary>
/// Shows a confirmation dialog with the given <paramref name="configuration"/>
/// and waits for the user to make a choice. Their choice is returned as a <see cref="bool"/>.
/// </summary>
/// <param name="configuration">The configuration for the dialog.</param>
/// <returns>Whether the user confirmed the dialog.</returns>
public static async UniTask<bool> ConfirmAsync(ConfirmConfiguration configuration)
{
ConfirmDialog dialog = dialogGameObject.Value.AddComponent<ConfirmDialog>();
dialog.OneTime = true;
await UniTask.WaitUntil(() => dialog.Dialog != null); // May need to wait for initialization.
dialog.ShowMenu(configuration);
return await dialog.OnChoiceMade.OnInvokeAsync(CancellationToken.None);
}
}
}
3 changes: 3 additions & 0 deletions Assets/SEE/UI/Menu/ConfirmDialog.cs.meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5c29c87d6b544ee2b20d225feaf7b824
timeCreated: 1731674969
79 changes: 0 additions & 79 deletions Assets/SEE/UI/Menu/Drawable/ConfirmDialogMenu.cs

This file was deleted.

11 changes: 0 additions & 11 deletions Assets/SEE/UI/Menu/Drawable/ConfirmDialogMenu.cs.meta

This file was deleted.

Loading

0 comments on commit 1865e76

Please sign in to comment.