mirror of
https://github.com/AlexMacocian/MonoGame.Extended.git
synced 2026-07-24 12:06:37 +00:00
361 lines
16 KiB
C#
361 lines
16 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using Microsoft.Xna.Framework.Input;
|
|
using MonoGame.Extended.Input;
|
|
using MonoGame.Extended.Input.InputListeners;
|
|
using MonoGame.Extended.NuclexGui.Input;
|
|
|
|
namespace MonoGame.Extended.NuclexGui.Controls
|
|
{
|
|
/// <summary>Represents an element in the user interface</summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Controls are always arranged in a tree where each control except the one at
|
|
/// the root of the tree has exactly one owner (the one at the root has no owner).
|
|
/// The design actively prevents you from assigning a control as child to
|
|
/// multiple parents.
|
|
/// </para>
|
|
/// <para>
|
|
/// The controls in the Nuclex.UserInterface library are fully independent of
|
|
/// their graphical representation. That means you can construct a dialog
|
|
/// without even having a graphics device in place, that you can move your
|
|
/// dialogs between different graphics devices and that you do not have to
|
|
/// even think about graphics device resets and similar trouble.
|
|
/// </para>
|
|
/// </remarks>
|
|
public partial class GuiControl
|
|
{
|
|
/// <summary>Whether this control can obtain the input focus</summary>
|
|
private readonly bool _affectsOrdering;
|
|
|
|
/// <summary>Child controls belonging to this control</summary>
|
|
/// <remarks>
|
|
/// Child controls are any controls that belong to this control. They don't
|
|
/// neccessarily need to be situated in this control's client area, but
|
|
/// their positioning will be relative to the parent's location.
|
|
/// </remarks>
|
|
private readonly ParentingControlCollection _children;
|
|
|
|
/// <summary>Name of the control instance (for programmatic identification)</summary>
|
|
private string _name;
|
|
|
|
/// <summary>Control this control is contained in</summary>
|
|
private GuiControl _parent;
|
|
|
|
/// <summary>GUI instance this control has been added to. Can be null.</summary>
|
|
private GuiScreen _screen;
|
|
|
|
/// <summary>Initializes a new control</summary>
|
|
public GuiControl() : this(false)
|
|
{
|
|
}
|
|
|
|
/// <summary>Initializes a new control</summary>
|
|
/// <param name="affectsOrdering">
|
|
/// Whether the control comes to the top of the hierarchy when clicked
|
|
/// </param>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The <paramref name="affectsOrdering" /> parameter should be set for windows
|
|
/// and other free-floating panels which exist in parallel and which the user
|
|
/// might want to put on top of their siblings by clicking them. If the user
|
|
/// clicks on a child control of such a panel/window control, the panel/window
|
|
/// control will also be moved into the foreground.
|
|
/// </para>
|
|
/// <para>
|
|
/// It should not be set for normal controls which usually have no overlap,
|
|
/// like buttons. Otherwise, a button placed on the desktop could overdraw a
|
|
/// window when the button is clicked. The behavior would be well-defined and
|
|
/// controlled, but the user probably doesn't expect this ;-)
|
|
/// </para>
|
|
/// </remarks>
|
|
protected GuiControl(bool affectsOrdering)
|
|
{
|
|
_affectsOrdering = affectsOrdering;
|
|
|
|
_children = new ParentingControlCollection(this);
|
|
}
|
|
|
|
/// <summary>Location and extents of the control</summary>
|
|
public UniRectangle Bounds;
|
|
|
|
/// <summary>Children of the control</summary>
|
|
public Collection<GuiControl> Children => _children;
|
|
|
|
/// <summary>
|
|
/// True if clicking the control or its children moves the control into
|
|
/// the foreground of the drawing hierarchy
|
|
/// </summary>
|
|
public bool AffectsOrdering => _affectsOrdering;
|
|
|
|
/// <summary>Parent control this control is contained in</summary>
|
|
/// <remarks>
|
|
/// Can be null, but this is only the case for free-floating controls that have
|
|
/// not been added into a Gui. The only control that really keeps this field
|
|
/// set to null whilst the Gui is active is the root control in the Gui class.
|
|
/// </remarks>
|
|
public GuiControl Parent => _parent;
|
|
|
|
/// <summary>Name that can be used to uniquely identify the control</summary>
|
|
/// <remarks>
|
|
/// This name acts as an unique identifier for a control. It primarily serves
|
|
/// as a means to programmatically identify the control and as a debugging aid.
|
|
/// Duplicate names are not allowed and will result in an exception being
|
|
/// thrown, the only exception is when the control's name is set to null.
|
|
/// </remarks>
|
|
public string Name
|
|
{
|
|
get { return _name; }
|
|
set
|
|
{
|
|
// Don't do anything if we're given the same name we already have. This
|
|
// is not a pure performance optimization, it also prevents the control
|
|
// from reporting an name collision with itself in this special case :)
|
|
if (value != _name)
|
|
{
|
|
// Look for name collisions with our siblings
|
|
var parent = Parent;
|
|
if (parent != null)
|
|
{
|
|
if (parent._children.IsNameTaken(value))
|
|
throw new InvalidOperationException("Another control is already using this name");
|
|
}
|
|
|
|
// Everything seems to be ok, accept the new name
|
|
_name = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>GUI instance this control belongs to. Can be null.</summary>
|
|
internal GuiScreen Screen => _screen;
|
|
|
|
/// <summary>Control the mouse is currently over</summary>
|
|
protected internal GuiControl MouseOverControl => _mouseOverControl;
|
|
|
|
/// <summary>Control that currently captured incoming input</summary>
|
|
protected internal GuiControl ActivatedControl => _activatedControl;
|
|
|
|
/// <summary>Moves the control into the foreground</summary>
|
|
public void BringToFront()
|
|
{
|
|
// Doing nothing if we don't have a parent is okay since in that case,
|
|
// we're the root and we're the frontmost control in any case. If the user
|
|
// calls BringToFront() on a control before he integrates it into the GUI
|
|
// tree, this is expected behavior and only logical.
|
|
var control = this;
|
|
while (!ReferenceEquals(control._parent, null))
|
|
{
|
|
var siblings = control._parent._children;
|
|
siblings.MoveToStart(siblings.IndexOf(control));
|
|
|
|
control = control._parent;
|
|
}
|
|
}
|
|
|
|
/// <summary>Obtains the absolute boundaries of the control in screen coordinates</summary>
|
|
/// <returns>The control's absolute screen coordinate boundaries</returns>
|
|
/// <remarks>
|
|
/// This method resolves the unified coordinates into absolute screen coordinates
|
|
/// that can be used to do hit-testing and rendering. The control is required to
|
|
/// be part of a GUI hierarchy that is assigned to a screen for this to work
|
|
/// since otherwise, there's no absolute coordinate frame into which the
|
|
/// unified coordinates could be resolved.
|
|
/// </remarks>
|
|
public RectangleF GetAbsoluteBounds()
|
|
{
|
|
// Is this the topmost control in the hierarchy (the desktop control)?
|
|
if (ReferenceEquals(_parent, null))
|
|
{
|
|
// Make sure the control is attached to a screen, otherwise, it's a free
|
|
// control not living in any GUI hierarchy and thus, does not have
|
|
// absolute bounds yet.
|
|
if (ReferenceEquals(_screen, null))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Obtaining absolute bounds requires the control to be part of a screen"
|
|
);
|
|
}
|
|
|
|
// Transform the unified coordinate bounds into absolute pixel coordinates
|
|
// for the screen's dimensions
|
|
return Bounds.ToOffset(_screen.Width, _screen.Height);
|
|
}
|
|
// Control is the child of another control
|
|
|
|
// Recursively determine the bounds of the parent control until we end up
|
|
// at the desktop control (or not, if this is a free living hierarchy, in
|
|
// which case the exception above will be triggered as soon as the top of
|
|
// the hierarchy is reached)
|
|
var parentBounds = _parent.GetAbsoluteBounds();
|
|
|
|
// Determine the controls absolute position based on the absolute
|
|
// dimensions and position of the parent control
|
|
var controlBounds = Bounds.ToOffset(parentBounds.Width, parentBounds.Height);
|
|
controlBounds.Offset(parentBounds.X, parentBounds.Y);
|
|
|
|
// Done, controlBounds now contains the absolute screen coordinates of
|
|
// the control's boundaries.
|
|
return controlBounds;
|
|
}
|
|
|
|
/// <summary>Called when an input command was sent to the control</summary>
|
|
/// <param name="command">Input command that has been triggered</param>
|
|
/// <returns>Whether the command has been processed by the control</returns>
|
|
protected virtual bool OnCommand(Command command)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Called when a button on the gamepad has been pressed</summary>
|
|
/// <param name="button">Button that has been pressed</param>
|
|
/// <returns>True if the button press was handled by the control, otherwise false.</returns>
|
|
/// <remarks>
|
|
/// If the control indicates that it didn't handle the key press, it will not
|
|
/// receive the associated key release notification.
|
|
/// </remarks>
|
|
protected virtual bool OnButtonPressed(Buttons button)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Called when a button on the gamepad has been released</summary>
|
|
/// <param name="button">Button that has been released</param>
|
|
protected virtual void OnButtonReleased(Buttons button)
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when the mouse position is updated</summary>
|
|
/// <param name="x">X coordinate of the mouse cursor on the control</param>
|
|
/// <param name="y">Y coordinate of the mouse cursor on the control</param>
|
|
protected virtual void OnMouseMoved(float x, float y)
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when a mouse button has been pressed down</summary>
|
|
/// <param name="button">Index of the button that has been pressed</param>
|
|
/// <returns>Whether the control has processed the mouse press</returns>
|
|
/// <remarks>
|
|
/// If this method states that a mouse press is processed by returning
|
|
/// true, that means the control did something with it and the mouse press
|
|
/// should not be acted upon by any other listener.
|
|
/// </remarks>
|
|
protected virtual void OnMousePressed(MouseButton button)
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when a mouse button has been released again</summary>
|
|
/// <param name="button">Index of the button that has been released</param>
|
|
protected virtual void OnMouseReleased(MouseButton button)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the mouse has left the control and is no longer hovering over it
|
|
/// </summary>
|
|
protected virtual void OnMouseLeft(float x, float y)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the mouse has entered the control and is now hovering over it
|
|
/// </summary>
|
|
protected virtual void OnMouseEntered()
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when the mouse wheel has been rotated</summary>
|
|
/// <param name="ticks">Number of ticks that the mouse wheel has been rotated</param>
|
|
protected virtual void OnMouseWheel(float ticks)
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when a key on the keyboard has been pressed down</summary>
|
|
/// <param name="keyCode">Code of the key that was pressed</param>
|
|
/// <returns>
|
|
/// True if the key press was handled by the control, otherwise false.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// If the control indicates that it didn't handle the key press, it will not
|
|
/// receive the associated key release notification. This means that if you
|
|
/// return false from this method, you should under no circumstances do anything
|
|
/// with the information - you will not know when the key is released again
|
|
/// and another control might pick it up, causing a second key response.
|
|
/// </remarks>
|
|
protected virtual bool OnKeyPressed(Keys keyCode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Called when a key on the keyboard has been released again</summary>
|
|
/// <param name="keyCode">Code of the key that was released</param>
|
|
protected virtual void OnKeyReleased(Keys keyCode)
|
|
{
|
|
}
|
|
|
|
/// <summary>Called when a command was sent to the control</summary>
|
|
/// <param name="command">Command to be injected</param>
|
|
/// <returns>Whether the command has been processed</returns>
|
|
internal bool ProcessCommand(Command command)
|
|
{
|
|
switch (command)
|
|
{
|
|
// These are not supported on the control level
|
|
case Command.SelectPrevious:
|
|
case Command.SelectNext:
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// These can be handled by user code if he so wishes
|
|
case Command.Up:
|
|
case Command.Down:
|
|
case Command.Left:
|
|
case Command.Right:
|
|
case Command.Accept:
|
|
case Command.Cancel:
|
|
{
|
|
return OnCommand(command);
|
|
}
|
|
|
|
// Value not contained in enumation - should not be happening!
|
|
default:
|
|
{
|
|
throw new ArgumentException("Invalid command", "command");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Assigns a new parent to the control</summary>
|
|
/// <param name="parent">New parent to assign to the control</param>
|
|
internal void SetParent(GuiControl parent)
|
|
{
|
|
_parent = parent;
|
|
|
|
// Have we been assigned to a parent?
|
|
if (_parent != null)
|
|
{
|
|
// If this ownership change transferred us to a different gui, we will
|
|
// have to migrate our visual and also the visuals of all our children.
|
|
if (!ReferenceEquals(_screen, parent._screen))
|
|
SetScreen(parent._screen);
|
|
}
|
|
else
|
|
{
|
|
// No parent, we're now officially an orphan ;)
|
|
|
|
// Orphans don't have screens!
|
|
SetScreen(null);
|
|
}
|
|
}
|
|
|
|
/// <summary>Assigns a new GUI to the control</summary>
|
|
/// <param name="gui">New GUI to assign to the control</param>
|
|
internal void SetScreen(GuiScreen gui)
|
|
{
|
|
_screen = gui;
|
|
|
|
_children.SetScreen(gui);
|
|
}
|
|
}
|
|
} |