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;
}
}