using System; using System.Collections.Generic; using System.Linq; using Robust.Client.Audio; using Robust.Client.ResourceManagement; using Robust.Shared.ContentPack; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface.Controls { /// /// Base class for a generic UI button. /// /// /// /// public abstract class BaseButton : Control { private int _attemptingPress; private bool _beingHovered; private bool _disabled; private bool _pressed; private bool _enableAllKeybinds; private ButtonGroup? _group; private bool _toggleMode; private bool _muteSounds; /// /// Specifies the group this button belongs to. /// /// /// Of multiple buttons in the same group, only one can be pressed (radio buttons). /// public ButtonGroup? Group { get => _group; set { if (value?.InternalButtons.Contains(this) ?? false) return; // No work to do. // Remove from old group. _group?.InternalButtons.Remove(this); _group = value; if (value == null) { return; } value.InternalButtons.Add(this); ToggleMode = true; if (value.IsNoneSetAllowed) { // Still UNPRESS if there's another pressed button, but don't PRESS it otherwise. if (value.Pressed != this) _pressed = false; } else { // Set us to pressed if we're the first button. Doesn't go through the setter to avoid setting off our own error check. _pressed = value.InternalButtons.Count == 1; } DrawModeChanged(); } } /// /// Controls mode of operation in relation to press/release events. /// [ViewVariables] public ActionMode Mode { get; set; } = ActionMode.Release; /// /// Whether the button is disabled. /// If a button is disabled, it appears greyed out and cannot be interacted with. /// [ViewVariables] public bool Disabled { get => _disabled; set { var old = _disabled; _disabled = value; if (old != value) { DrawModeChanged(); } } } /// /// Whether the button is currently toggled down. Only applies when is true. /// [ViewVariables] public bool Pressed { get => _pressed; set { if (_pressed == value) { return; } if (!value && Group is { IsNoneSetAllowed: false }) { throw new InvalidOperationException("Cannot directly unset a grouped button. Set another button in the group instead."); } _pressed = value; if (Group != null && _pressed) { UnsetOtherGroupButtons(); } DrawModeChanged(); } } /// /// Sets the button's press state and also handles click sounds. /// /// public void SetClickPressed(bool value) { Pressed = value; if (Pressed != value) return; if (!MuteSounds) UserInterfaceManager.ClickSound(); } /// /// Whether key functions other than trigger the button. /// [ViewVariables] public bool EnableAllKeybinds { get => _enableAllKeybinds; set => _enableAllKeybinds = value; } /// /// If true, this button functions as a toggle, not as a regular push button. /// [ViewVariables] public bool ToggleMode { get => _toggleMode; set { if (Group != null && !value) { throw new InvalidOperationException("Cannot disable toggle mode on a button in a group."); } _toggleMode = value; } } /// /// If true, this button is currently being hovered over by the mouse. /// [ViewVariables] public bool IsHovered => _beingHovered; /// /// Draw mode used for styling of buttons. /// [ViewVariables] public DrawModeEnum DrawMode { get { if (Disabled) { return DrawModeEnum.Disabled; } else if (Pressed || (_attemptingPress > 0 && IsHovered)) { return DrawModeEnum.Pressed; } else if (IsHovered || _attemptingPress > 0) { return DrawModeEnum.Hover; } else { return DrawModeEnum.Normal; } } } /// /// If true, this button will not emit sounds when the mouse is pressed or hovered over. /// [ViewVariables] public bool MuteSounds { get => _muteSounds; set => _muteSounds = value; } /// /// Fired when the button is pushed down by the mouse. /// public event Action? OnButtonDown; /// /// Fired when the button is released by the mouse. /// public event Action? OnButtonUp; /// /// Fired when the button is "pressed". When this happens depends on . /// public event Action? OnPressed; /// /// If is set, fired when the button is toggled up or down. /// public event Action? OnToggled; protected BaseButton() { MouseFilter = MouseFilterMode.Stop; } protected virtual void DrawModeChanged() { } protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (Disabled || args.Function == EngineKeyFunctions.Use || (!_enableAllKeybinds && args.Function != EngineKeyFunctions.UIClick)) { return; } var buttonEventArgs = new ButtonEventArgs(this, args); OnButtonDown?.Invoke(buttonEventArgs); var drawMode = DrawMode; if (Mode == ActionMode.Release) { UserInterfaceManager.ControlFocused = this; _attemptingPress += 1; } else { if (ToggleMode) { // Can't un press a radio button directly. if (Group == null || !Pressed) { SetClickPressed(!Pressed); OnPressed?.Invoke(buttonEventArgs); OnToggled?.Invoke(new ButtonToggledEventArgs(Pressed, this, args)); UnsetOtherGroupButtons(); } } else { UserInterfaceManager.ControlFocused = this; _attemptingPress += 1; OnPressed?.Invoke(buttonEventArgs); } } if (drawMode != DrawMode) { DrawModeChanged(); } } protected internal override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); if (Disabled || args.Function == EngineKeyFunctions.Use || (!_enableAllKeybinds && args.Function != EngineKeyFunctions.UIClick)) { return; } var buttonEventArgs = new ButtonEventArgs(this, args); OnButtonUp?.Invoke(buttonEventArgs); var drawMode = DrawMode; if (Mode == ActionMode.Release && _attemptingPress > 0 && HasPoint((args.PointerLocation.Position - GlobalPixelPosition) / UIScale)) { // Can't un press a radio button directly. // Only trigger toggle on UIClick. Do not un-press a toggle button if it's in a group. if (args.Function != EngineKeyFunctions.UIClick || Group == null || !Pressed) { if (args.Function == EngineKeyFunctions.UIClick && ToggleMode && _attemptingPress == 1) { SetClickPressed(!Pressed); } else { if (!MuteSounds) UserInterfaceManager.ClickSound(); } OnPressed?.Invoke(buttonEventArgs); if (args.Function == EngineKeyFunctions.UIClick && ToggleMode) { OnToggled?.Invoke(new ButtonToggledEventArgs(Pressed, this, args)); if (_pressed) { UnsetOtherGroupButtons(); } } } } if (_attemptingPress > 0) _attemptingPress -= 1; if (_attemptingPress <= 0 && UserInterfaceManager.ControlFocused == this) UserInterfaceManager.ControlFocused = null; if (drawMode != DrawMode) { DrawModeChanged(); } } protected internal override void ControlFocusExited() { base.ControlFocusExited(); var drawMode = DrawMode; _attemptingPress = 0; if (drawMode != DrawMode) { DrawModeChanged(); } } private void UnsetOtherGroupButtons() { if (_group == null) { return; } foreach (var button in _group.InternalButtons) { if (button != this && button.Pressed) { button._pressed = false; button.DrawModeChanged(); } } } protected internal override void MouseEntered() { base.MouseEntered(); if (!Disabled && !MuteSounds) { UserInterfaceManager.HoverSound(); } var drawMode = DrawMode; _beingHovered = true; if (drawMode != DrawMode) { DrawModeChanged(); } } protected internal override void MouseExited() { base.MouseExited(); var drawMode = DrawMode; _beingHovered = false; if (drawMode != DrawMode) { DrawModeChanged(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); Group = null; } public enum DrawModeEnum : byte { Normal = 0, Pressed = 1, Hover = 2, Disabled = 3 } [Virtual] public class ButtonEventArgs : EventArgs { /// /// The button this event originated from. /// public BaseButton Button { get; } public GUIBoundKeyEventArgs Event { get; } public ButtonEventArgs(BaseButton button, GUIBoundKeyEventArgs args) { Button = button; Event = args; } } /// /// Fired when a is toggled. /// public sealed class ButtonToggledEventArgs : ButtonEventArgs { /// /// The new pressed state of the button. /// public bool Pressed { get; } public ButtonToggledEventArgs(bool pressed, BaseButton button, GUIBoundKeyEventArgs args) : base(button, args) { Pressed = pressed; } } /// /// For use with . /// public enum ActionMode : byte { /// /// fires when the mouse button causing them is pressed down. /// Press = 0, /// /// fires when the mouse button causing them is released. /// This is the default and most intuitive method. /// Release = 1 } } /// /// Represents a group of buttons. /// /// /// Of all buttons in a group, only one can be pressed down. /// Yes, it's for radio buttons. /// public sealed class ButtonGroup { /// /// Whether it is legal for this button group to have no selected button. /// /// /// If true, it's legal for no button in the group to be active. /// This is then the initial state of a new group of buttons (no button is automatically selected), /// and it becomes legal to manually clear the active button through code. /// The user cannot manually unselect the active button regardless, only by selecting a difference button. /// public bool IsNoneSetAllowed { get; } /// /// Create a new /// /// The value of on the new button group. public ButtonGroup(bool isNoneSetAllowed = true) { IsNoneSetAllowed = isNoneSetAllowed; } internal readonly List InternalButtons = new(); public IReadOnlyList Buttons => InternalButtons; public BaseButton? Pressed => InternalButtons.FirstOrDefault(x => x.Pressed); } }