UI/LevelEditorShortcutInput.cs
using Sandbox.UI;

namespace BlockParty;

public sealed class LevelEditorShortcutInput : Panel
{
	public LevelEditorStage Stage { get; set; }

	private readonly NavRepeater _horizontalRepeat = new();
	private readonly NavRepeater _verticalRepeat = new();
	private bool _leftHeld;
	private bool _rightHeld;
	private bool _upHeld;
	private bool _downHeld;

	private bool _suspended;

	/// <summary>While an overlay (the level browser) covers the canvas, presses are ignored so shortcuts
	/// can't edit the hidden level. Releases still pass through so held nudge/modifier state can't stick,
	/// and focus stays here so the stage's raw-keyboard hotkeys stay dead too.</summary>
	public bool Suspended
	{
		get => _suspended;
		set
		{
			if ( _suspended == value ) return;
			_suspended = value;
			if ( value ) _leftHeld = _rightHeld = _upHeld = _downHeld = false;
		}
	}

	public LevelEditorShortcutInput()
	{
		AcceptsFocus = true;
	}

	public override string GetClipboardValue( bool cut )
	{
		return !Suspended && !cut && Stage?.CopySelectedObstacle() == true ? "blockparty:obstacle" : null;
	}

	public override void OnPaste( string text )
	{
		if ( !Suspended ) Stage?.PasteObstacle();
	}

	public void TickRepeat( float dt )
	{
		if ( Stage is null ) return;

		int dx = _horizontalRepeat.Tick( dt, _leftHeld, _rightHeld );
		int dy = _verticalRepeat.Tick( dt, _downHeld, _upHeld );
		if ( dx != 0 || dy != 0 ) Stage.NudgeSelected( dx, dy );
	}

	public override void OnButtonEvent( ButtonEvent e )
	{
		if ( Suspended && e.Pressed )
		{
			base.OnButtonEvent( e );
			return;
		}

		// Keep modified WASD presses available to shortcuts such as Ctrl-S, but always process releases.
		if ( SetNudgeHeld( e.Button, e.Pressed, !e.HasCtrl ) )
		{
			e.StopPropagation = true;
			return;
		}

		if ( ( e.Button == "alt" || e.Button == "lalt" || e.Button == "ralt" ) && Stage is not null )
		{
			Stage.SetSnapInvertHeld( e.Pressed );
			e.StopPropagation = true;
			return;
		}

		// Additive-selection modifiers: forwarded like Alt/X because the stage's raw-keyboard reads
		// are unreliable while this panel holds focus. Not consumed — Shift/Ctrl also modify other
		// shortcuts (Shift+M, Ctrl+Shift+Z), which read them from the event's HasShift/HasCtrl.
		if ( e.Button == "shift" || e.Button == "lshift" || e.Button == "rshift" )
		{
			Stage?.SetShiftModifierHeld( e.Pressed );
			base.OnButtonEvent( e );
			return;
		}

		if ( e.Button == "ctrl" || e.Button == "lctrl" || e.Button == "rctrl" || e.Button == "control" )
		{
			Stage?.SetCtrlModifierHeld( e.Pressed );
			base.OnButtonEvent( e );
			return;
		}

		if ( e.Button == "x" && Stage is not null )
		{
			Stage.SetCardinalDragHeld( e.Pressed );
			e.StopPropagation = true;
			return;
		}

		if ( !e.Pressed || Stage is null )
		{
			base.OnButtonEvent( e );
			return;
		}

		if ( e.HasCtrl && e.Button == "z" )
		{
			if ( e.HasShift ) Stage.Redo();
			else Stage.Undo();
			e.StopPropagation = true;
			return;
		}

		if ( e.HasCtrl && e.Button == "y" )
		{
			Stage.Redo();
			e.StopPropagation = true;
			return;
		}

		if ( e.HasCtrl && e.Button == "a" )
		{
			Stage.SelectAll();
			e.StopPropagation = true;
			return;
		}

		// Ctrl+C/V are NOT handled here: the engine converts them into Copy/PasteEvents, delivered
		// via GetClipboardValue/OnPaste above — handling the raw ButtonEvent too would double-fire.

		if ( e.HasCtrl && e.Button == "s" )
		{
			Stage.SaveToProject();
			e.StopPropagation = true;
			return;
		}

		bool handled = true;
		switch ( e.Button )
		{
			case "e" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Select; Stage.Rebuild(); break;
			case "b" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Spawn; Stage.Rebuild(); break;
			case "p" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Player; Stage.Rebuild(); break;
			case "q" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Obstacle; Stage.Rebuild(); break;
			case "o" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Coin; Stage.Rebuild(); break;
			case "k" when !e.HasCtrl: Stage.Tool = LevelEditorStage.EditTool.Spike; Stage.Rebuild(); break;
			case "v" when !e.HasCtrl: Stage.ToggleSelectedObstacleVision(); break;
			case "m" when !e.HasCtrl && e.HasShift: Stage.MirrorY = !Stage.MirrorY; Stage.Rebuild(); break;
			case "m" when !e.HasCtrl: Stage.MirrorX = !Stage.MirrorX; Stage.Rebuild(); break;
			case "f" when !e.HasCtrl: Stage.ToggleSelectedObstacleFence(); break;
			case "g" when !e.HasCtrl: Stage.ToggleSelectedObstacleGlass(); break;
			case "delete":
			case "backspace": Stage.DeleteSelected(); break;
			default: handled = false; break;
		}

		if ( handled ) e.StopPropagation = true;
		else base.OnButtonEvent( e );
	}

	private bool SetNudgeHeld( string button, bool held, bool allowLetterPress )
	{
		int dx = 0, dy = 0;
		bool changed;
		switch ( button )
		{
			case "a" when allowLetterPress || !held:
			case "left":
			case "leftarrow": changed = _leftHeld != held; _leftHeld = held; dx = -1; break;
			case "d" when allowLetterPress || !held:
			case "right":
			case "rightarrow": changed = _rightHeld != held; _rightHeld = held; dx = 1; break;
			case "w" when allowLetterPress || !held:
			case "up":
			case "uparrow": changed = _upHeld != held; _upHeld = held; dy = 1; break;
			case "s" when allowLetterPress || !held:
			case "down":
			case "downarrow": changed = _downHeld != held; _downHeld = held; dy = -1; break;
			default: return false;
		}

		// Prime the repeaters on the event so their next emitted step comes after RepeatDelay.
		_horizontalRepeat.Tick( 0f, _leftHeld, _rightHeld );
		_verticalRepeat.Tick( 0f, _downHeld, _upHeld );
		if ( changed && held ) Stage?.NudgeSelected( dx, dy );
		return true;
	}
}