/// <summary>
/// Helper class for simplified creation, configuration, and management of Virt-A-Mate UI elements and screen message overlays.
/// </summary>
public class UIHelper
: IDisposable
{
private GameObject _msgOverlayGO = null;
private UnityEngine.UI.Text _msgOverlayText = null;
private float _msgOverlayTimer = 0f;
/* consts */
/// <summary>
/// Default text color for on-screen overlay messages.
/// </summary>
public readonly Color DEFAULT_COLOR = Color.green;
/// <summary>
/// Default alpha transparency for on-screen overlay messages.
/// </summary>
public const float DEFAULT_ALPHA = 0.5f;
/// <summary>
/// Default display duration in seconds for on-screen overlay messages.
/// </summary>
public const float DEFAULT_DELAY = 2f;
/* ctor/dtor */
/// <summary>
/// Initializes a new instance of the <see cref="UIHelper"/> class and constructs the 3D message overlay canvas.
/// </summary>
/// <param name="pluginName">The name of the parent plugin used as a prefix in log messages.</param>
public UIHelper(
string pluginName)
{
PluginName = pluginName;
_makeMessageOverlay();
}
/// <summary>
/// Releases all Unity UI GameObjects and resources used by the <see cref="UIHelper"/> instance.
/// </summary>
public void Dispose()
{
_dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed = false;
/// <summary>
/// Protected implementation of the Dispose pattern to safely destroy managed Unity objects.
/// </summary>
/// <param name="disposing">True if called explicitly from <see cref="Dispose"/>; false if called from a finalizer.</param>
protected virtual void _dispose(
bool disposing)
{
if (_disposed)
return;
if (disposing)
_destroyMessageOverlay();
_disposed = true;
}
/* readonly properties */
/// <summary>
/// Gets the name of the associated plugin.
/// </summary>
public string PluginName { get; }
/* methods */
/// <summary>
/// Centralized service for outputting informational plugin messages to the system log and optionally to the 3D screen overlay.
/// </summary>
/// <param name="text">The message text to display.</param>
/// <param name="showInUI">Flag indicating whether to force display the message on the user's 3D-View overlay.</param>
/// <param name="textColor">Optional custom text color for the on-screen overlay.</param>
/// <param name="delay">Optional custom display duration in seconds before fading out.</param>
public void OutMessage(
string text,
bool showInUI = false,
Color? textColor = null,
float? delay = null)
{
try
{
string formattedMsg1 = $"[{PluginName}] {text}";
SuperController.LogMessage(formattedMsg1);
if (showInUI)
ShowMessage(text, textColor, delay);
}
catch (Exception e)
{
SuperController.LogError($"[{PluginName}] UIHelper.OutMessage() Critical Error: {e.Message}");
}
}
/// <summary>
/// Centralized service for outputting plugin error messages to the system error log and optionally to the 3D screen overlay.
/// </summary>
/// <param name="text">The error message text to display.</param>
/// <param name="showInUI">Flag indicating whether to force display the error on the user's 3D-View overlay.</param>
/// <param name="textColor">Optional custom text color (defaults to red if null).</param>
/// <param name="delay">Optional custom display duration in seconds before fading out.</param>
public void OutError(
string text,
bool showInUI = false,
Color? textColor = null,
float? delay = null)
{
try
{
string formattedErr1 = $"[{PluginName}] {text}";
SuperController.LogError(formattedErr1);
if (showInUI)
ShowMessage(text, textColor ?? Color.red, delay);
}
catch (Exception e)
{
SuperController.LogError($"[{PluginName}] UIHelper.OutError() Critical Error: {e.Message}");
}
}
/// <summary>
/// Checks for a physical keyboard key press based on the selected value in a <see cref="JSONStorableStringChooser"/>
/// and safely invokes the associated action delegate.
/// </summary>
/// <param name="chooser">The dropdown chooser instance containing the configured hotkey name.</param>
/// <param name="action">The action delegate to execute when the hotkey is pressed.</param>
public void TestHotkey(
JSONStorableStringChooser chooser,
Action action)
{
if (chooser == null || action == null)
return;
try
{
if (chooser.val != "None"
&& Input.GetKeyDown((KeyCode)Enum.Parse(typeof(KeyCode), chooser.val)))
action();
}
catch (Exception e)
{
OutError($"UIHelper.TestHotkey() [Param: {chooser.name}] Error: {e.Message}");
}
}
/// <summary>
/// Activates the 3D overlay canvas, resets text opacity, and displays the specified message string.
/// </summary>
/// <param name="message">The text string to render on the overlay.</param>
/// <param name="textColor">Optional text color (uses <see cref="DEFAULT_COLOR"/> if null).</param>
/// <param name="delay">Optional lifetime in seconds (uses <see cref="DEFAULT_DELAY"/> if null).</param>
public void ShowMessage(
string message,
Color? textColor = null,
float? delay = null)
{
if (_msgOverlayText != null && _msgOverlayGO != null)
{
_msgOverlayText.text = message;
var color1 = textColor ?? DEFAULT_COLOR;
color1.a = DEFAULT_ALPHA;
_msgOverlayText.color = color1;
_msgOverlayGO.SetActive(true);
_msgOverlayTimer = delay ?? DEFAULT_DELAY;
}
}
/// <summary>
/// Ticks every frame in the plugin's Update loop to decrement the overlay timer and apply a smooth alpha fade-out effect.
/// </summary>
public void MuteMessage()
{
if (_msgOverlayGO != null && _msgOverlayTimer > 0f)
{
_msgOverlayTimer -= Time.unscaledDeltaTime;
if (_msgOverlayTimer <= 0f)
_msgOverlayGO.SetActive(false);
else if (_msgOverlayText != null && _msgOverlayTimer < 1f)
{
var color1 = _msgOverlayText.color;
color1.a = _msgOverlayTimer * DEFAULT_ALPHA;
_msgOverlayText.color = color1;
}
}
}
/* privates */
/// <summary>
/// Constructs the WorldSpace UI Canvas and Text components parented to the center monitor camera.
/// </summary>
private void _makeMessageOverlay()
{
_msgOverlayGO = new GameObject("UIHelper_MessageOverlay");
var targetCam1 = SuperController.singleton.lookCamera ?? SuperController.singleton.MonitorCenterCamera;
if (targetCam1 != null)
_msgOverlayGO.transform.SetParent(targetCam1.transform, false);
_msgOverlayGO.transform.localPosition = new Vector3(0f, 0f, 1f);
var canvas1 = _msgOverlayGO.AddComponent<Canvas>();
canvas1.renderMode = RenderMode.WorldSpace;
_msgOverlayGO.AddComponent<CanvasScaler>().dynamicPixelsPerUnit = 100;
var canvasRect1 = _msgOverlayGO.GetComponent<RectTransform>();
canvasRect1.sizeDelta = new Vector2(800f, 200f);
canvasRect1.localScale = new Vector3(0.003f, 0.003f, 0.003f);
var text1 = new GameObject("UIHelper_MessageOverlay_Text");
text1.transform.SetParent(_msgOverlayGO.transform, false);
_msgOverlayText = text1.AddComponent<UnityEngine.UI.Text>();
_msgOverlayText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
_msgOverlayText.fontSize = 32;
_msgOverlayText.alignment = TextAnchor.MiddleCenter;
var color1 = DEFAULT_COLOR;
color1.a = DEFAULT_ALPHA;
_msgOverlayText.color = color1;
var textRect1 = text1.GetComponent<RectTransform>();
textRect1.anchoredPosition = Vector2.zero;
textRect1.sizeDelta = new Vector2(800f, 200f);
_msgOverlayGO.SetActive(false);
}
/// <summary>
/// Destroys the overlay GameObject if it exists.
/// </summary>
private void _destroyMessageOverlay()
{
if (_msgOverlayGO != null)
UnityEngine.Object.Destroy(_msgOverlayGO);
}
// SuperController.AlertUser("Hello from my plugin!", () => SuperController.LogMessage("User clicked OK"));
// SuperController.AlertUser("Reset all settings?",
// () => SuperController.LogMessage("Confirmed"), // OK
// () => SuperController.LogMessage("Cancelled")); // Cancel
}