using Robust.Client.UserInterface.Controls; using Robust.Shared.Timing; namespace Content.Client.UserInterface.Controls; /// /// A Button that requires a second click to actually invoke its OnPressed action.
/// When clicked once it will change rendering modes to be prefixed by /// and displays on the button instead of .
///
/// After the first click needs to elapse before it can be clicked again to confirm.
/// When the button doesn't get clicked a second time before passes it changes back to its normal state.
///
/// /// Colors for the different states need to be set in the stylesheet /// public sealed partial class ConfirmButton : Button { [Dependency] private IGameTiming _gameTiming = default!; public const string ConfirmPrefix = "confirm-"; /// /// The time when the button will revert from confirming if left unpressed. /// private TimeSpan? _nextReset; /// /// The time when the button should re-enable itself (to avoid debouncing). /// private TimeSpan? _nextCooldown; private bool _isConfirming; private string? _confirmationText; private string? _text; /// /// Fired when the button was pressed and confirmed /// public new event Action? OnPressed; /// /// Fired when the button has started to confirm and is awaiting a second button press. /// public event Action? OnConfirming; /// /// /// Hides the buttons text property to be able to sanely replace the button text with /// when asking for confirmation /// public new string? Text { get => _text; set { _text = value; UpdateText(); } } /// /// /// Overrides the confirming state of the button when set. /// Intended for public use, should not be used inside the class due to IsConfirming side-effects. /// public new bool Disabled { get => base.Disabled; set { // Force out of confirming status (may change base.Disabled), then write your new disabled value. SetIsConfirming(false); SetDisabled(value); } } /// /// The text displayed on the button when waiting for a second click /// [ViewVariables(VVAccess.ReadWrite)] public string ConfirmationText { get => _confirmationText ?? Loc.GetString("generic-confirm"); set { _confirmationText = value; UpdateText(); } } /// /// The time until the button reverts to normal /// [ViewVariables(VVAccess.ReadWrite)] public TimeSpan ResetTime { get; set; } = TimeSpan.FromSeconds(2); /// /// The time until the button accepts a second click. This is to prevent accidentally confirming the button /// [ViewVariables(VVAccess.ReadWrite)] public TimeSpan CooldownTime { get; set; } = TimeSpan.FromSeconds(.5); /// /// A property to get or change whether the button is confirming (awaiting a second press within a time limit) or not /// [ViewVariables] public bool IsConfirming { get => _isConfirming; set { if (_isConfirming != value) SetIsConfirming(value); } } public ConfirmButton() { IoCManager.InjectDependencies(this); base.OnPressed += HandleOnPressed; } protected override void FrameUpdate(FrameEventArgs args) { if (!IsConfirming) return; if (_gameTiming.CurTime > _nextReset) SetIsConfirming(false); else if (Disabled && _gameTiming.CurTime > _nextCooldown) SetDisabled(false); } protected override void DrawModeChanged() { if (IsConfirming) { switch (DrawMode) { case DrawModeEnum.Normal: SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassNormal); break; case DrawModeEnum.Pressed: SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassPressed); break; case DrawModeEnum.Hover: SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassHover); break; case DrawModeEnum.Disabled: SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassDisabled); break; default: throw new ArgumentOutOfRangeException(); } } else { base.DrawModeChanged(); } } private void HandleOnPressed(ButtonEventArgs buttonEvent) { //Prevent accidental confirmations from double clicking if (IsConfirming && _nextCooldown > _gameTiming.CurTime) return; // Update the state machine before invoking our events. SetIsConfirming(!IsConfirming); switch (IsConfirming) { case true: OnConfirming?.Invoke(buttonEvent); break; case false: OnPressed?.Invoke(buttonEvent); break; } } /// /// Updates the text shown on the button depending on whether the button is confirming or not. /// private void UpdateText() { base.Text = IsConfirming ? ConfirmationText : Text; } /// /// Internal setter logi /// private void SetDisabled(bool value) { base.Disabled = value; } /// /// Internal setter logic for /// Sets the text, the timers, and the disabled state. /// private void SetIsConfirming(bool value) { _isConfirming = value; // Update button visuals. UpdateText(); DrawModeChanged(); if (value) { // Start our timers, disable button until cooldown. SetDisabled(true); _nextCooldown = _gameTiming.CurTime + CooldownTime; _nextReset = _gameTiming.CurTime + ResetTime; } else { // Clear timers, ensure button is enabled if it was disabled before (previous SetIsConfirming(true) call => valid reset timer) if (Disabled && _nextReset != null) SetDisabled(false); _nextReset = null; _nextCooldown = null; } } }