mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
971 lines
33 KiB
C#
971 lines
33 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using JetBrains.Annotations;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Shared.Animations;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Robust.Client.UserInterface
|
|
{
|
|
/// <summary>
|
|
/// A node in the GUI system.
|
|
/// See https://hackmd.io/@ss14/ui-system-tutorial for some basic concepts.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public partial class Control : IDisposable
|
|
{
|
|
private readonly List<Control> _orderedChildren = new();
|
|
|
|
private bool _visible = true;
|
|
|
|
// _marginSetSize is the size calculated by the margins,
|
|
// but it's different from _size if min size is higher.
|
|
|
|
private bool _canKeyboardFocus;
|
|
|
|
public event Action<Control>? OnVisibilityChanged;
|
|
|
|
/// <summary>
|
|
/// The name of this control.
|
|
/// Names must be unique between the siblings of the control.
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public string? Name { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, this control will always be rendered, even if other UI rendering is disabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Useful for e.g. primary viewports.
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)] public bool AlwaysRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Our parent inside the control tree.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This cannot be changed directly. Use <see cref="AddChild" /> and such on the parent to change it.
|
|
/// </remarks>
|
|
[ViewVariables]
|
|
public Control? Parent { get; private set; }
|
|
|
|
public NameScope? NameScope;
|
|
|
|
//public void AttachNameScope(Dictionary<string, Control> nameScope)
|
|
//{
|
|
// _nameScope = nameScope;
|
|
//}
|
|
|
|
public NameScope? FindNameScope()
|
|
{
|
|
foreach (var control in this.GetSelfAndLogicalAncestors())
|
|
{
|
|
if (control.NameScope != null) return control.NameScope;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public T FindControl<T>(string name) where T : Control
|
|
{
|
|
var nameScope = FindNameScope();
|
|
if (nameScope == null)
|
|
{
|
|
throw new ArgumentException("No Namespace found for Control");
|
|
}
|
|
|
|
var value = nameScope.Find(name);
|
|
if (value == null)
|
|
{
|
|
throw new ArgumentException($"No Control with the name {name} found");
|
|
}
|
|
|
|
if (value is not T ret)
|
|
{
|
|
throw new ArgumentException($"Control with name {name} had invalid type {value.GetType()}");
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
internal IUserInterfaceManagerInternal UserInterfaceManagerInternal { get; }
|
|
|
|
/// <summary>
|
|
/// The UserInterfaceManager we belong to, for convenience.
|
|
/// </summary>
|
|
public IUserInterfaceManager UserInterfaceManager => UserInterfaceManagerInternal;
|
|
|
|
/// <summary>
|
|
/// Gets an ordered enumerable over all the children of this control.
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public OrderedChildCollection Children { get; }
|
|
|
|
[Content]
|
|
public virtual ICollection<Control> XamlChildren { get; protected set; }
|
|
|
|
[ViewVariables] public int ChildCount => _orderedChildren.Count;
|
|
|
|
/// <summary>
|
|
/// Gets whether this control is at all visible.
|
|
/// This means the control is part of the tree of the root control, and all of its parents are visible.
|
|
/// </summary>
|
|
/// <seealso cref="Visible"/>
|
|
[ViewVariables]
|
|
public bool VisibleInTree
|
|
{
|
|
get
|
|
{
|
|
for (var parent = this; parent != null; parent = parent.Parent)
|
|
{
|
|
if (!parent.Visible)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (parent == UserInterfaceManager.RootControl)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Whether or not this control and its children are visible.
|
|
/// </summary>
|
|
/// <seealso cref="VisibleInTree"/>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[Animatable]
|
|
public bool Visible
|
|
{
|
|
get => _visible;
|
|
set
|
|
{
|
|
if (_visible == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_visible = value;
|
|
|
|
_propagateVisibilityChanged(value);
|
|
// TODO: unhardcode this.
|
|
// Many containers ignore children if they're invisible, so that's why we're replicating that ehre.
|
|
Parent?.InvalidateMeasure();
|
|
InvalidateMeasure();
|
|
}
|
|
}
|
|
|
|
private void _propagateVisibilityChanged(bool newVisible)
|
|
{
|
|
OnVisibilityChanged?.Invoke(this);
|
|
if (!VisibleInTree)
|
|
{
|
|
UserInterfaceManagerInternal.ControlHidden(this);
|
|
}
|
|
|
|
foreach (var child in _orderedChildren)
|
|
{
|
|
if (newVisible || child._visible)
|
|
{
|
|
child._propagateVisibilityChanged(newVisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether or not this control is an (possibly indirect) child of
|
|
/// <see cref="IUserInterfaceManager.RootControl"/>
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public bool IsInsideTree => Root != null;
|
|
|
|
[ViewVariables]
|
|
public virtual UIRoot? Root { get; internal set; }
|
|
|
|
private void _propagateExitTree()
|
|
{
|
|
Root = null;
|
|
_exitedTree();
|
|
|
|
foreach (var child in _orderedChildren)
|
|
{
|
|
child._propagateExitTree();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the control is removed from the root control tree.
|
|
/// </summary>
|
|
/// <seealso cref="EnteredTree"/>
|
|
protected virtual void ExitedTree()
|
|
{
|
|
}
|
|
|
|
private void _exitedTree()
|
|
{
|
|
ExitedTree();
|
|
UserInterfaceManagerInternal.ControlRemovedFromTree(this);
|
|
}
|
|
|
|
private void _propagateEnterTree(UIRoot root)
|
|
{
|
|
Root = root;
|
|
_enteredTree();
|
|
|
|
foreach (var child in _orderedChildren)
|
|
{
|
|
child._propagateEnterTree(root);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the control enters the root control tree.
|
|
/// </summary>
|
|
/// <seealso cref="ExitedTree"/>
|
|
protected virtual void EnteredTree()
|
|
{
|
|
}
|
|
|
|
private void _enteredTree()
|
|
{
|
|
EnteredTree();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Simple text tooltip that is shown when the mouse is hovered over this control for a bit.
|
|
/// See <see cref="TooltipSupplier"/> or <see cref="OnShowTooltip"/> for a more customizable alternative.
|
|
/// No effect when TooltipSupplier is specified.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If empty or null, no tooltip is shown in the first place (but OnShowTooltip and OnHideTooltip
|
|
/// events are still fired).
|
|
/// </remarks>
|
|
public string? ToolTip { get; set; }
|
|
|
|
/// <summary>
|
|
/// Overrides the global tooltip delay, showing the tooltip for this
|
|
/// control within the specified number of seconds.
|
|
/// </summary>
|
|
public float? TooltipDelay { get; set; }
|
|
|
|
/// <summary>
|
|
/// When a tooltip should be shown for this control, this will be invoked to
|
|
/// produce a control which will serve as the tooltip (doing nothing if null is returned).
|
|
/// This is the generally recommended way to implement custom tooltips for controls, as it takes
|
|
/// care of the various edge cases for showing / hiding the tooltip.
|
|
/// For an even more customizable approach, <see cref="OnShowTooltip"/>
|
|
///
|
|
/// The returned control will be added to PopupRoot, and positioned
|
|
/// within the user interface under the current mouse position to avoid going off the edge of the
|
|
/// screen. When the tooltip should be hidden, the control will be hidden by removing it from the tree.
|
|
///
|
|
/// It is expected that the returned control remains within PopupRoot. Other classes should
|
|
/// not move it around in the tree or move it out of PopupRoot, but may access and modify
|
|
/// the control and its children via <see cref="SuppliedTooltip"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Returning a new instance of a tooltip control every time is usually fine. If for some
|
|
/// reason constructing the tooltip control is expensive, it MAY be fine to cache + reuse a single instance but this
|
|
/// approach has not yet been tested.
|
|
/// </remarks>
|
|
public TooltipSupplier? TooltipSupplier { get; set; }
|
|
|
|
/// <summary>
|
|
/// Invoked when the mouse is hovered over this control for a bit and a tooltip
|
|
/// should be shown. Can be used as an alternative to ToolTip or TooltipSupplier to perform custom tooltip
|
|
/// logic such as showing a more complex tooltip control.
|
|
///
|
|
/// Any custom tooltip controls should typically be added
|
|
/// as a child of UserInterfaceManager.PopupRoot
|
|
/// Handlers can use <see cref="Tooltips.PositionTooltip(Control)"/> to assist with positioning
|
|
/// custom tooltip controls.
|
|
/// </summary>
|
|
public event EventHandler? OnShowTooltip;
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// If this control is currently showing a tooltip provided via TooltipSupplier,
|
|
/// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot.
|
|
/// Also, as it may be hidden (removed from tree) at any time, saving a reference to this is a Bad Idea.
|
|
/// </summary>
|
|
public Control? SuppliedTooltip => UserInterfaceManagerInternal.GetSuppliedTooltipFor(this);
|
|
|
|
/// <summary>
|
|
/// Manually hide the tooltip currently being shown for this control, if there is one.
|
|
/// </summary>
|
|
public void HideTooltip()
|
|
{
|
|
UserInterfaceManagerInternal.HideTooltipFor(this);
|
|
}
|
|
|
|
internal void PerformShowTooltip()
|
|
{
|
|
OnShowTooltip?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when this control is showing a tooltip which should now be hidden.
|
|
/// </summary>
|
|
public event EventHandler? OnHideTooltip;
|
|
|
|
internal void PerformHideTooltip()
|
|
{
|
|
OnHideTooltip?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The mode that controls how mouse filtering works. See the enum for how it functions.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public MouseFilterMode MouseFilter { get; set; } = MouseFilterMode.Ignore;
|
|
|
|
/// <summary>
|
|
/// Whether this control can take keyboard focus.
|
|
/// Keyboard focus is necessary for the control to receive keyboard events.
|
|
/// </summary>
|
|
/// <seealso cref="KeyboardFocusOnClick"/>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool CanKeyboardFocus
|
|
{
|
|
get => _canKeyboardFocus;
|
|
set
|
|
{
|
|
if (_canKeyboardFocus == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_canKeyboardFocus = value;
|
|
|
|
if (!value)
|
|
{
|
|
ReleaseKeyboardFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the control will automatically receive keyboard focus (if possible) when clicked on.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Obviously, <see cref="CanKeyboardFocus"/> must be set to true for this to work.
|
|
/// </remarks>
|
|
public bool KeyboardFocusOnClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether to clip drawing of this control and its children to its rectangle.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// By default, controls (and their children) can render outside their rectangle.
|
|
/// If this is set, rendering is hard clipped to it.
|
|
/// </remarks>
|
|
/// <seealso cref="RectDrawClipMargin"/>
|
|
[ViewVariables]
|
|
public bool RectClipContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// A margin around this control. If this control + this margin is outside its parent's <see cref="RectClipContent" />,
|
|
/// it will not be drawn.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A control rectangle does not necessarily have to be listened to for drawing.
|
|
/// So the problem is, how do we know where to stop trying to draw the control if it's clipped away?
|
|
/// </remarks>
|
|
/// <seealso cref="RectClipContent"/>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int RectDrawClipMargin { get; set; } = 10;
|
|
|
|
// You may wonder why Modulate isn't stylesheet controlled, but ModulateSelf is.
|
|
// Reason is simple: I'm fucking lazy.
|
|
// I'm expecting this comment to last much longer than the problem it's pointing out.
|
|
|
|
/// <summary>
|
|
/// An override for the modulate self from the style sheet.
|
|
/// </summary>
|
|
/// <seealso cref="ActualModulateSelf" />
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public Color? ModulateSelfOverride { get; set; }
|
|
|
|
/// <summary>
|
|
/// Modulates the color of this control and all its children when drawing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Modulation is multiplying or tinting the color basically.
|
|
/// </remarks>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[Animatable]
|
|
public Color Modulate { get; set; } = Color.White;
|
|
|
|
/// <summary>
|
|
/// The value used to modulate this control (and not its siblings) with on top of <see cref="Modulate"/>
|
|
/// when drawing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// By default this value is pulled from CSS, or <see cref="ModulateSelfOverride"/> if available.
|
|
///
|
|
/// Modulation is multiplying or tinting the color basically.
|
|
/// </remarks>
|
|
public Color ActualModulateSelf
|
|
{
|
|
get
|
|
{
|
|
if (ModulateSelfOverride.HasValue)
|
|
{
|
|
return ModulateSelfOverride.Value;
|
|
}
|
|
|
|
if (TryGetStyleProperty(StylePropertyModulateSelf, out Color modulate))
|
|
{
|
|
return modulate;
|
|
}
|
|
|
|
return Color.White;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default constructor.
|
|
/// The name of the control is decided based on type.
|
|
/// </summary>
|
|
public Control()
|
|
{
|
|
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
|
|
StyleClasses = new StyleClassCollection(this);
|
|
Children = new OrderedChildCollection(this);
|
|
XamlChildren = Children;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called to render this control.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Drawing is done relative to the position of the control.
|
|
/// It is also done in pixel space, so you should not directly use properties such as <see cref="Size"/>.
|
|
/// </remarks>
|
|
/// <param name="handle">A handle that can be used to draw.</param>
|
|
protected internal virtual void Draw(DrawingHandleScreen handle)
|
|
{
|
|
}
|
|
|
|
public virtual void DrawInternal(IRenderHandle renderHandle)
|
|
{
|
|
Draw(renderHandle.DrawingHandleScreen);
|
|
}
|
|
|
|
public void UpdateDraw()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when this modal control is closed.
|
|
/// Only used for controls that are actually modals.
|
|
/// </summary>
|
|
protected internal virtual void ModalRemoved()
|
|
{
|
|
}
|
|
|
|
public bool Disposed { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Dispose this control, its own scene control, and all its children.
|
|
/// Basically the big delete button.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (Disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dispose(true);
|
|
Disposed = true;
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UserInterfaceManagerInternal.HideTooltipFor(this);
|
|
|
|
DisposeAllChildren();
|
|
Parent?.RemoveChild(this);
|
|
|
|
OnKeyBindDown = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose all children, but leave this one intact.
|
|
/// </summary>
|
|
public void DisposeAllChildren()
|
|
{
|
|
// Cache because the children modify the dictionary.
|
|
var children = new List<Control>(Children);
|
|
foreach (var child in children)
|
|
{
|
|
child.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all the children from this control.
|
|
/// </summary>
|
|
public void RemoveAllChildren()
|
|
{
|
|
DebugTools.Assert(!Disposed, "Control has been disposed.");
|
|
|
|
foreach (var child in Children.ToArray())
|
|
{
|
|
RemoveChild(child);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make this child an orphan. i.e. remove it from its parent if it has one.
|
|
/// </summary>
|
|
public void Orphan()
|
|
{
|
|
DebugTools.Assert(!Disposed, "Control has been disposed.");
|
|
|
|
Parent?.RemoveChild(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make the provided control a parent of this control.
|
|
/// </summary>
|
|
/// <param name="child">The control to make a child of this control.</param>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// Thrown if we already have a component with the same name,
|
|
/// or the provided component is still parented to a different control.
|
|
/// </exception>
|
|
/// <exception cref="ArgumentNullException">
|
|
/// <paramref name="child" /> is <c>null</c>.
|
|
/// </exception>
|
|
public void AddChild(Control child)
|
|
{
|
|
DebugTools.Assert(!Disposed, "Control has been disposed.");
|
|
|
|
if (child == null) throw new ArgumentNullException(nameof(child));
|
|
if (child.Parent != null)
|
|
{
|
|
throw new InvalidOperationException("This component is still parented. Deparent it before adding it.");
|
|
}
|
|
|
|
DebugTools.Assert(!child.Disposed, "Child is disposed.");
|
|
|
|
if (child == this)
|
|
{
|
|
throw new InvalidOperationException("You can't parent something to itself!");
|
|
}
|
|
|
|
// Ensure this control isn't a parent of ours.
|
|
// Doesn't need to happen if the control has no children of course.
|
|
if (child.ChildCount != 0)
|
|
{
|
|
for (var parent = Parent; parent != null; parent = parent.Parent)
|
|
{
|
|
if (parent == child)
|
|
{
|
|
throw new ArgumentException("This control is one of our parents!", nameof(child));
|
|
}
|
|
}
|
|
}
|
|
|
|
child.Parent = this;
|
|
_orderedChildren.Add(child);
|
|
|
|
child.Parented(this);
|
|
if (Root != null)
|
|
{
|
|
child._propagateEnterTree(Root);
|
|
}
|
|
|
|
ChildAdded(child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after a new child is added to this control.
|
|
/// </summary>
|
|
/// <param name="newChild">The new child.</param>
|
|
protected virtual void ChildAdded(Control newChild)
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when this control gets made a child of a different control.
|
|
/// </summary>
|
|
/// <param name="newParent">The new parent component.</param>
|
|
protected virtual void Parented(Control newParent)
|
|
{
|
|
StylesheetUpdateRecursive();
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the provided child from this control.
|
|
/// </summary>
|
|
/// <param name="child">The child to remove.</param>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// Thrown if the provided child is not one of this control's children.
|
|
/// </exception>
|
|
public void RemoveChild(Control child)
|
|
{
|
|
DebugTools.Assert(!Disposed, "Control has been disposed.");
|
|
|
|
if (child.Parent != this)
|
|
{
|
|
throw new InvalidOperationException("The provided control is not a direct child of this control.");
|
|
}
|
|
|
|
_orderedChildren.Remove(child);
|
|
|
|
child.Parent = null;
|
|
|
|
child.Deparented();
|
|
if (IsInsideTree)
|
|
{
|
|
child._propagateExitTree();
|
|
}
|
|
|
|
ChildRemoved(child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a child is removed from this child.
|
|
/// </summary>
|
|
/// <param name="child">The former child.</param>
|
|
protected virtual void ChildRemoved(Control child)
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when this control is removed as child from the former parent.
|
|
/// </summary>
|
|
protected virtual void Deparented()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the order index of a child changes.
|
|
/// </summary>
|
|
/// <param name="child">The child that was changed.</param>
|
|
/// <param name="oldIndex">The previous index of the child.</param>
|
|
/// <param name="newIndex">The new index of the child.</param>
|
|
protected virtual void ChildMoved(Control child, int oldIndex, int newIndex)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called to test whether this control has a certain point,
|
|
/// for the purposes of finding controls under the cursor.
|
|
/// </summary>
|
|
/// <param name="point">The relative point, in virtual pixels.</param>
|
|
/// <returns>True if this control does have the point and should be counted as a hit.</returns>
|
|
protected internal virtual bool HasPoint(Vector2 point)
|
|
{
|
|
var size = Size;
|
|
return point.X >= 0 && point.X <= size.X && point.Y >= 0 && point.Y <= size.Y;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the immediate child of this control with the specified index.
|
|
/// </summary>
|
|
/// <param name="index">The index of the child.</param>
|
|
/// <returns>The child.</returns>
|
|
public Control GetChild(int index)
|
|
{
|
|
return _orderedChildren[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the "index" in the parent.
|
|
/// This index is used for ordering of actions like input and drawing among siblings.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// Thrown if this control has no parent.
|
|
/// </exception>
|
|
public int GetPositionInParent()
|
|
{
|
|
if (Parent == null)
|
|
{
|
|
throw new InvalidOperationException("This control has no parent!");
|
|
}
|
|
|
|
return Parent._orderedChildren.IndexOf(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the index of this control in the parent.
|
|
/// This pretty much corresponds to layout and drawing order in relation to its siblings.
|
|
/// </summary>
|
|
/// <param name="position"></param>
|
|
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
|
|
public void SetPositionInParent(int position)
|
|
{
|
|
if (Parent == null)
|
|
{
|
|
throw new InvalidOperationException("No parent to change position in.");
|
|
}
|
|
|
|
var posInParent = GetPositionInParent();
|
|
if (posInParent == position)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Parent._orderedChildren.RemoveAt(posInParent);
|
|
Parent._orderedChildren.Insert(position, this);
|
|
Parent.ChildMoved(this, posInParent, position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes this the first control among its siblings,
|
|
/// So that it's first in things such as drawing order.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
|
|
public void SetPositionFirst()
|
|
{
|
|
SetPositionInParent(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes this the last control among its siblings,
|
|
/// So that it's last in things such as drawing order.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
|
|
public void SetPositionLast()
|
|
{
|
|
if (Parent == null)
|
|
{
|
|
throw new InvalidOperationException("No parent to change position in.");
|
|
}
|
|
|
|
SetPositionInParent(Parent.ChildCount - 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when this control receives keyboard focus.
|
|
/// </summary>
|
|
protected internal virtual void KeyboardFocusEntered()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when this control loses keyboard focus (corresponds to UserInterfaceManager.KeyboardFocused).
|
|
/// </summary>
|
|
protected internal virtual void KeyboardFocusExited()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fired when a control loses control focus for any reason. See <see cref="IUserInterfaceManager.ControlFocused"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Controls which have some sort of drag / drop behavior should usually implement this method (typically by cancelling the drag drop).
|
|
/// Otherwise, if a user clicks down LMB over one control to initiate a drag, then clicks RMB down
|
|
/// over a different control while still holding down LMB, the control being dragged will now lose focus
|
|
/// and will no longer receive the keyup for the LMB, thus won't cancel the drag.
|
|
/// This should also be considered for controls which have any special KeyBindUp behavior - consider
|
|
/// what would happen if the control lost focus and never received the KeyBindUp.
|
|
///
|
|
/// There is no corresponding ControlFocusEntered - if a control wants to handle that situation they should simply
|
|
/// handle KeyBindDown as that's the only way a control would gain focus.
|
|
/// </remarks>
|
|
protected internal virtual void ControlFocusExited()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if this control currently has keyboard focus.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public virtual bool HasKeyboardFocus()
|
|
{
|
|
return UserInterfaceManager.KeyboardFocused == this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grab keyboard focus if this control doesn't already have it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <see cref="CanKeyboardFocus"/> must be true for this to work.
|
|
/// </remarks>
|
|
public void GrabKeyboardFocus()
|
|
{
|
|
UserInterfaceManager.GrabKeyboardFocus(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Release keyboard focus from this control if it has it.
|
|
/// If a different control has keyboard focus, nothing happens.
|
|
/// </summary>
|
|
public void ReleaseKeyboardFocus()
|
|
{
|
|
UserInterfaceManager.ReleaseKeyboardFocus(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the size of the control changes.
|
|
/// </summary>
|
|
protected virtual void Resized() { }
|
|
|
|
internal void DoFrameUpdate(FrameEventArgs args)
|
|
{
|
|
FrameUpdate(args);
|
|
foreach (var child in Children)
|
|
{
|
|
child.DoFrameUpdate(args);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is called before every render frame.
|
|
/// </summary>
|
|
protected virtual void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
ProcessAnimations(args);
|
|
}
|
|
|
|
// These are separate from StandardCursorShape so that
|
|
// in the future we could have an API to override the styling.
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"{Name} ({GetType().Name})";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mode that will be tested when testing controls to invoke mouse button events on.
|
|
/// </summary>
|
|
public enum MouseFilterMode : byte
|
|
{
|
|
/// <summary>
|
|
/// The control will be able to receive mouse buttons events.
|
|
/// Furthermore, if a control with this mode does get clicked,
|
|
/// the event automatically gets marked as handled after every other candidate has been tried,
|
|
/// so that the rest of the game does not receive it.
|
|
/// </summary>
|
|
Pass = 1,
|
|
|
|
/// <summary>
|
|
/// The control will be able to receive mouse button events like <see cref="Pass" />,
|
|
/// but the event will be stopped and handled even if the relevant events do not handle it.
|
|
/// </summary>
|
|
Stop = 0,
|
|
|
|
/// <summary>
|
|
/// The control will not be considered at all, and will not have any effects.
|
|
/// </summary>
|
|
Ignore = 2,
|
|
}
|
|
|
|
public class OrderedChildCollection : ICollection<Control>, IReadOnlyCollection<Control>
|
|
{
|
|
private readonly Control Owner;
|
|
|
|
public OrderedChildCollection(Control owner)
|
|
{
|
|
Owner = owner;
|
|
}
|
|
|
|
public Enumerator GetEnumerator()
|
|
{
|
|
return new(Owner);
|
|
}
|
|
|
|
IEnumerator<Control> IEnumerable<Control>.GetEnumerator() => GetEnumerator();
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
public void Add(Control item)
|
|
{
|
|
Owner.AddChild(item);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
Owner.RemoveAllChildren();
|
|
}
|
|
|
|
public bool Contains(Control item)
|
|
{
|
|
return item?.Parent == Owner;
|
|
}
|
|
|
|
public void CopyTo(Control[] array, int arrayIndex)
|
|
{
|
|
Owner._orderedChildren.CopyTo(array, arrayIndex);
|
|
}
|
|
|
|
public bool Remove(Control item)
|
|
{
|
|
if (item?.Parent != Owner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DebugTools.AssertNotNull(Owner);
|
|
Owner.RemoveChild(item);
|
|
|
|
return true;
|
|
}
|
|
|
|
int ICollection<Control>.Count => Owner.ChildCount;
|
|
int IReadOnlyCollection<Control>.Count => Owner.ChildCount;
|
|
|
|
public bool IsReadOnly => false;
|
|
|
|
|
|
public struct Enumerator : IEnumerator<Control>
|
|
{
|
|
private List<Control>.Enumerator _enumerator;
|
|
|
|
internal Enumerator(Control control)
|
|
{
|
|
_enumerator = control._orderedChildren.GetEnumerator();
|
|
}
|
|
|
|
public bool MoveNext()
|
|
{
|
|
return _enumerator.MoveNext();
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
((IEnumerator) _enumerator).Reset();
|
|
}
|
|
|
|
public Control Current => _enumerator.Current;
|
|
|
|
object IEnumerator.Current => Current;
|
|
|
|
public void Dispose()
|
|
{
|
|
_enumerator.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public delegate Control? TooltipSupplier(Control sender);
|
|
}
|