Editor/NumericInputSession.cs

Editor UI helper for modal numeric input. NumericInputSession stores buffered numeric characters, parses them to a float using invariant culture, exposes activation lifecycle and editing operations (append digit, decimal, toggle sign, backspace, confirm). NumericInputShortcuts wires keyboard shortcuts to the active NumericInputSession via ModalOperationArbiter.

Reflection
#nullable enable
using Editor;
using System.Globalization;

namespace BlenderActions;

/// <summary>Stores and parses numeric input for one modal transform operation.</summary>
public sealed class NumericInputSession
{
    /// <summary>Stores buffered numeric input characters.</summary>
    private string _text = string.Empty;
    /// <summary>Tracks whether numeric or modal confirmation has been requested.</summary>
    private bool _confirmRequested;

    /// <summary>Gets whether this numeric input session is accepting input.</summary>
    public bool IsActive { get; private set; }
    /// <summary>Attempts to parse the buffered text as an invariant floating-point value.</summary>
    public bool HasValue => TryGetValue(out _);
    /// <summary>Handles is null or empty.</summary>
    public string DisplayText => string.IsNullOrEmpty(_text) ? "0" : _text;

    /// <summary>Binds the operation to the active scene context and starts its modal lifecycle.</summary>
    public void Begin()
    {
        _text = string.Empty;
        _confirmRequested = false;
        IsActive = true;
    }

    /// <summary>Ends numeric input and clears its buffered state.</summary>
    public void End()
    {
        _text = string.Empty;
        _confirmRequested = false;
        IsActive = false;
    }

    /// <summary>Attempts to parse the buffered text as an invariant floating-point value.</summary>
    public bool TryGetValue(out float value)
    {
        if(!IsActive)
        {
            value = 0f;
            return false;
        }

        return float.TryParse(
            _text,
            NumberStyles.Float,
            CultureInfo.InvariantCulture,
            out value);
    }

    /// <summary>Consumes and clears a pending numeric confirmation request.</summary>
    public bool ConsumeConfirmRequest()
    {
        if(!_confirmRequested)
            return false;

        _confirmRequested = false;
        return true;
    }

    /// <summary>Appends one digit while numeric input is active.</summary>
    public void AppendDigit(char digit)
    {
        if(IsActive)
            _text += digit;
    }

    /// <summary>Appends a decimal separator when one is not already present.</summary>
    public void EnterDecimal()
    {
        if(!IsActive || _text.Contains('.'))
            return;

        _text = string.IsNullOrEmpty(_text)
            ? "0."
            : _text == "-"
                ? "-0."
                : _text + '.';
    }

    /// <summary>Toggles the sign of the buffered numeric value.</summary>
    public void ToggleNegative()
    {
        if(!IsActive)
            return;

        _text = _text.StartsWith("-")
            ? _text[1..]
            : "-" + _text;
    }

    /// <summary>Removes the last buffered numeric character.</summary>
    public void Backspace()
    {
        if(IsActive && _text.Length > 0)
            _text = _text[..^1];
    }

    /// <summary>Requests confirmation when the buffered numeric value is valid.</summary>
    public void Confirm()
    {
        if(HasValue)
            _confirmRequested = true;
    }
}

/// <summary>Routes numeric keyboard shortcuts to the active modal operation.</summary>
public static class NumericInputShortcuts
{
    /// <summary>Appends the digit zero to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_0", "0", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Zero() => Append('0');
    /// <summary>Appends the digit one to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_1", "1", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void One() => Append('1');
    /// <summary>Appends the digit two to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_2", "2", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Two() => Append('2');
    /// <summary>Appends the digit three to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_3", "3", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Three() => Append('3');
    /// <summary>Appends the digit four to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_4", "4", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Four() => Append('4');
    /// <summary>Appends the digit five to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_5", "5", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Five() => Append('5');
    /// <summary>Appends the digit six to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_6", "6", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Six() => Append('6');
    /// <summary>Appends the digit seven to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_7", "7", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Seven() => Append('7');
    /// <summary>Appends the digit eight to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_8", "8", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Eight() => Append('8');
    /// <summary>Appends the digit nine to active numeric input.</summary>
    [Shortcut("blender_actions.numeric_9", "9", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Nine() => Append('9');

    /// <summary>Routes decimal input to the active numeric session.</summary>
    [Shortcut("blender_actions.numeric_decimal", ".", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Decimal() => ModalOperationArbiter.NumericInput?.EnterDecimal();

    /// <summary>Routes sign toggling to the active numeric session.</summary>
    [Shortcut("blender_actions.numeric_negative", "-", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Negative() => ModalOperationArbiter.NumericInput?.ToggleNegative();

    /// <summary>Removes the last buffered numeric character.</summary>
    [Shortcut("blender_actions.numeric_backspace", "BACKSPACE", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Backspace() => ModalOperationArbiter.NumericInput?.Backspace();

    /// <summary>Requests confirmation when the buffered numeric value is valid.</summary>
    [Shortcut("blender_actions.numeric_confirm", "ENTER", typeof(SceneViewportWidget), ShortcutType.Widget)]
    private static void Confirm() => ModalOperationArbiter.NumericInput?.Confirm();

    /// <summary>Routes a digit to the active numeric session.</summary>
    private static void Append(char digit)
    {
        ModalOperationArbiter.NumericInput?.AppendDigit(digit);
    }
}