Editor/ViewportInputLock.cs

Editor utility class that temporarily disables selection and context-menu input on a scene view, viewport, and optional tools while a modal operation runs, and restores their previous states on disposal. It also tracks active locks to restore them during editor hotload.

Reflection
#nullable enable
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;

namespace BlenderActions;

/// <summary>Temporarily suppresses viewport selection and context-menu input during modal operations.</summary>
internal sealed class ViewportInputLock : IDisposable
{
    /// <summary>Handles new.</summary>
    private static readonly HashSet<ViewportInputLock> ActiveLocks = new();

    /// <summary>References the scene view bound to the active operation.</summary>
    private readonly SceneViewWidget _sceneView;
    /// <summary>References the scene viewport bound to the active operation.</summary>
    private readonly SceneViewportWidget _viewport;
    /// <summary>References the optional tool.</summary>
    private readonly EditorTool? _tool;
    /// <summary>References the optional sub tool.</summary>
    private readonly EditorTool? _subTool;

    /// <summary>Tracks whether scene read only.</summary>
    private readonly bool _sceneReadOnly;
    /// <summary>Tracks whether scene context menu.</summary>
    private readonly bool _sceneContextMenu;
    /// <summary>Tracks whether viewport read only.</summary>
    private readonly bool _viewportReadOnly;
    /// <summary>Tracks whether viewport context menu.</summary>
    private readonly bool _viewportContextMenu;
    /// <summary>Tracks whether tool selection.</summary>
    private readonly bool _toolSelection;
    /// <summary>Tracks whether tool context menu.</summary>
    private readonly bool _toolContextMenu;
    /// <summary>Tracks whether sub tool selection.</summary>
    private readonly bool _subToolSelection;
    /// <summary>Tracks whether sub tool context menu.</summary>
    private readonly bool _subToolContextMenu;

    /// <summary>Tracks whether this input lock has already restored its state.</summary>
    private bool _disposed;

    /// <summary>Initializes a new viewport input lock instance.</summary>
    public ViewportInputLock(
        SceneViewWidget sceneView,
        SceneViewportWidget viewport,
        EditorTool? tool,
        EditorTool? subTool)
    {
        _sceneView = sceneView;
        _viewport = viewport;
        _tool = tool;
        _subTool = subTool;

        _sceneReadOnly = sceneView.ReadOnly;
        _sceneContextMenu = sceneView.ContextMenuEnabled;
        _viewportReadOnly = viewport.ReadOnly;
        _viewportContextMenu = viewport.ContextMenuEnabled;

        sceneView.ReadOnly = true;
        sceneView.ContextMenuEnabled = false;
        viewport.ReadOnly = true;
        viewport.ContextMenuEnabled = false;

        if(tool != null)
        {
            _toolSelection = tool.AllowGameObjectSelection;
            _toolContextMenu = tool.AllowContextMenu;
            tool.AllowGameObjectSelection = false;
            tool.AllowContextMenu = false;
        }

        if(subTool != null && subTool != tool)
        {
            _subToolSelection = subTool.AllowGameObjectSelection;
            _subToolContextMenu = subTool.AllowContextMenu;
            subTool.AllowGameObjectSelection = false;
            subTool.AllowContextMenu = false;
        }

        ActiveLocks.Add(this);
    }

    /// <summary>Restores captured viewport and tool input state exactly once.</summary>
    public void Dispose()
    {
        if(_disposed)
            return;

        _disposed = true;
        ActiveLocks.Remove(this);

        Exception? restoreError = null;
        Restore(() =>
        {
            if(_sceneView.IsValid)
            {
                _sceneView.ReadOnly = _sceneReadOnly;
                _sceneView.ContextMenuEnabled = _sceneContextMenu;
            }
        }, ref restoreError);

        Restore(() =>
        {
            if(_viewport.IsValid)
            {
                _viewport.ReadOnly = _viewportReadOnly;
                _viewport.ContextMenuEnabled = _viewportContextMenu;
            }
        }, ref restoreError);

        Restore(() =>
        {
            if(_tool != null)
            {
                _tool.AllowGameObjectSelection = _toolSelection;
                _tool.AllowContextMenu = _toolContextMenu;
            }
        }, ref restoreError);

        Restore(() =>
        {
            if(_subTool != null && _subTool != _tool)
            {
                _subTool.AllowGameObjectSelection = _subToolSelection;
                _subTool.AllowContextMenu = _subToolContextMenu;
            }
        }, ref restoreError);

        if(restoreError != null)
            throw restoreError;
    }

    /// <summary>Restores every active input lock during editor hotload.</summary>
    [EditorEvent.Hotload]
    private static void RestoreAll()
    {
        var locks = new ViewportInputLock[ActiveLocks.Count];
        ActiveLocks.CopyTo(locks);

        foreach(var inputLock in locks)
        {
            try
            {
                inputLock.Dispose();
            }
            catch
            {
            }
        }

        ActiveLocks.Clear();
    }

    /// <summary>Executes one restoration step while retaining the first failure.</summary>
    private static void Restore(Action action, ref Exception? firstError)
    {
        try
        {
            action();
        }
        catch(Exception exception)
        {
            firstError ??= exception;
        }
    }
}