Files
MonoGame.Extended/Source/MonoGame.Extended.NuclexGui/Controls/GuiControlInput.cs
T

460 lines
21 KiB
C#

using System.Diagnostics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Input;
namespace MonoGame.Extended.NuclexGui.Controls
{
public partial class GuiControl
{
/// <summary>Control the mouse was pressed down on</summary>
private GuiControl _activatedControl;
/// <summary>Number of game pad buttons being held down</summary>
private int _heldButtonCount;
/// <summary>Number of keyboard keys being held down</summary>
private int _heldKeyCount;
/// <summary>Mouse buttons the user is holding down over the control</summary>
private MouseButton _heldMouseButtons;
/// <summary>Control the mouse is currently hovering over</summary>
private GuiControl _mouseOverControl;
/// <summary>Whether any keys, mouse buttons or game pad buttons are beind held pressed</summary>
private bool AnyKeysOrButtonsPressed => (_heldMouseButtons != 0) || (_heldKeyCount > 0) || (_heldButtonCount > 0);
/// <summary>Called when a button on the game pad has been pressed</summary>
/// <param name="button">Button that has been pressed</param>
/// <returns>
/// True if the button press was processed by the control and future game pad
/// input belongs to the control until all buttons are released again
/// </returns>
internal bool ProcessButtonPress(Buttons button)
{
// If there's an activated control (one being held down by the mouse or having
// accepted a previous button press), this control will get the button press
// delivered, whether it wants to or not.
if (_activatedControl != null)
{
++_heldButtonCount;
// If one of our children is the activated control, pass on the message
if (_activatedControl != this)
_activatedControl.ProcessButtonPress(button);
else
OnButtonPressed(button);
// We're already activated, so this button press is accepted in any case
return true;
}
// A button has been pressed but no control is activated currently. This means we
// have to look for a control which feels responsible for the button press,
// starting with ourselves.
// Does the user code in our derived class feel responsible for this button?
// If so, we're the new activated control and the button has been handled.
if (OnButtonPressed(button))
{
_activatedControl = this;
++_heldButtonCount;
return true;
}
// Nope, we have to ask our children to find a control that feels responsible.
var encounteredOrderingControl = false;
foreach (var child in _children)
{
// We only process one child that has the affectsOrdering field set. This
// ensures that key presses will not be delivered to windows sitting behind
// another window. Other siblings that are not windows are asked still, so
// a bunch of buttons on the desktop would be asked in addition to a window.
if (child._affectsOrdering)
{
if (encounteredOrderingControl)
continue;
encounteredOrderingControl = true;
}
// Does this child feel responsible for the button press?
if (child.ProcessButtonPress(button))
{
_activatedControl = child;
++_heldButtonCount;
return true;
}
}
// Neither we nor any of our children felt responsible for the button. Give up.
return false;
}
/// <summary>Called when a button on the game pad has been released</summary>
/// <param name="button">Button that has been released</param>
internal void ProcessButtonRelease(Buttons button)
{
// If we're the top level control, we will receive button presses and their related
// releases even if nobody was interested in the button presses. Thus, we silently
// ignore those presses we didn't accept.
if (_heldButtonCount == 0)
return;
// If we receive a release, we must have a control on which the mouse
// was pressed (possibly even ourselves)
Debug.Assert(
_activatedControl != null,
"ProcessButtonRelease() had no control a button was pressed on; " +
"ProcessButtonRelease() was called on a control instance, but the control " +
"did not register a prior button press for itself or any of its child controls"
);
--_heldButtonCount;
if (_activatedControl != this)
_activatedControl.ProcessButtonRelease(button);
else
OnButtonReleased(button);
// If no more keys buttons are being held down, clear the activated control
if (!AnyKeysOrButtonsPressed)
_activatedControl = null;
}
/// <summary>
/// Called when the mouse has left the control and is no longer hovering over it
/// </summary>
internal void ProcessMouseLeave(float x, float y)
{
// Because the mouse has left us, if we have a mouse-over control, it also
// cannot be over one of our children Children leaving the parent container
// are not supported by design and for consistency, the behavior is tweaked
// so the children are left when the parent is left - this avoids strange
// behavior like being able to select a control if entering it with the mouse
// from the container side but being unable to select it if entering from
// the outside.
if (_mouseOverControl != null)
{
if (_mouseOverControl != this)
_mouseOverControl.ProcessMouseLeave(x, y);
else
OnMouseLeft(x, y);
_mouseOverControl = null;
}
}
/// <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>
internal bool ProcessMousePress(MouseButton button)
{
// We remember the control the mouse was pressed over and won't replace it for
// as long as the mouse is being held down. This ensures the mouse release
// notification is always delivered to a control, even if the mouse is released
// after moving it away from the control.
if (_activatedControl == null)
{
_activatedControl = _mouseOverControl;
// If we received an initial mouse press outside of our control area,
// someone is feeding us notifications we shouldn't be receiving. The best
// thing we can do is ignore this notification. This is a normal situation
// for the top level control which does the input filtering.
if (_activatedControl == null)
return false;
// If we're a control that can appear on top of or below our siblings in
// the z order, bring us into foreground since the user just clicked on us.
if (_activatedControl != this)
{
if (_activatedControl._affectsOrdering)
_children.MoveToStart(_children.IndexOf(_activatedControl));
}
}
// Add the buttons to the list of mouse buttons being held down. This is used
// to track when we should clear the mouse-over control again.
_heldMouseButtons |= button;
// If the mouse is over another control, pass on the mouse press.
if (_activatedControl != this)
return _activatedControl.ProcessMousePress(button);
// Otherwise, the mouse press applies to us
// If this control can take the input focus, make it the focused control
if (_screen != null)
{
var focusable = this as IFocusable;
if ((focusable != null) && focusable.CanGetFocus)
_screen.FocusedControl = this;
}
// Deliver the notification to the control deriving from us
OnMousePressed(button);
return true;
}
/// <summary>Called when a mouse button has been released again</summary>
/// <param name="button">Index of the button that has been released</param>
internal void ProcessMouseRelease(MouseButton button)
{
// When the mouse is clicked on game window's border and the user drags it
// into the GUI area, we will get a rogue mouse release message without
// the related mouse press. We ignore such rogue mouse release messages.
if ((_heldMouseButtons & button) != button)
return;
// If we receive a release, we must have a control on which the mouse
// was pressed (possibly even ourselves)
Debug.Assert(
_activatedControl != null,
"ProcessMouseRelease() had no control the mouse was pressed on; " +
"ProcessMouseRelease() was called on a control instance, but the control " +
"did not register a prior mouse press over itself or any of its child controls"
);
// Remove the button from the list of mouse buttons being held down. This
// allows us to see when we can clear the mouse-press control.
_heldMouseButtons &= ~button;
// If the mouse was held over one of our childs, pass on the notification
if (_activatedControl != this)
_activatedControl.ProcessMouseRelease(button);
else
OnMouseReleased(button);
// If no more mouse buttons are being held down, clear the mouse-press control
if (!AnyKeysOrButtonsPressed)
_activatedControl = null;
}
/// <summary>Processes mouse movement notifications</summary>
/// <param name="containerWidth">Absolute width of the control's container</param>
/// <param name="containerHeight">Absolute height of the control's container</param>
/// <param name="x">Absolute X position of the mouse within the container</param>
/// <param name="y">Absolute Y position of the mouse within the container</param>
internal void ProcessMouseMove(float containerWidth, float containerHeight, float x, float y)
{
// Calculate the absolute pixel position and size of this control
var size = Bounds.Size.ToOffset(containerWidth, containerHeight);
// If a mouse button is being held down, the mouse movement notification is
// delivered to the control the mouse was pressed on first. This guarantees that
// windows can be dragged even if the mouse was close to the window border and
// leaves the window during dragging.
if (_activatedControl != null)
{
var mouseX = x - Bounds.Location.X.ToOffset(containerWidth);
var mouseY = y - Bounds.Location.Y.ToOffset(containerHeight);
// Deliver the mouse move notifcation (either to our own user code or
// to the control the mouse of hovering over)
if (_activatedControl != this)
_activatedControl.ProcessMouseMove(size.X, size.Y, mouseX, mouseY);
else
OnMouseMoved(mouseX, mouseY);
}
// Calculate the absolute mouse position. We cannot reuse the value calculated
// in the mouse-press handling code because the control could have been moved when
// we called OnMouseMoved() - a typical use case for draggable controls.
x -= Bounds.Location.X.ToOffset(containerWidth);
y -= Bounds.Location.Y.ToOffset(containerHeight);
// Check whether the mouse is hovering over one of our children and if so,
// pass on the mouse movement notification to the child.
foreach (var control in _children)
{
var childBounds = control.Bounds.ToOffset(size.X, size.Y);
// Is the mouse over this child?
if (childBounds.Contains(new Point2(x, y)))
{
SwitchMouseOverControl(control, x, y);
// Hand over the mouse movement data to the child control the mouse is
// hovering over. If this is the mouse-press control, do nothing because
// we already delivered the movement notification out of order.
if (_mouseOverControl != _activatedControl)
_mouseOverControl.ProcessMouseMove(size.X, size.Y, x, y);
// We got our mouse-over control, end processing.
return;
}
}
// The mouse was over none of our children, so it must be hovering over us,
// unless we're the control being pressed down, in which case we'd also be
// getting mouse movement data outside of our boundaries. In this case, we
// only should become the mouse-over control is actually over us.
if (
(x >= 0.0f) && (x < size.X) &&
(y >= 0.0f) && (y < size.Y)
)
{
SwitchMouseOverControl(this, x, y);
// If we weren't pressed, we didn't deliver the out-of-order update to
// our implementation. Send our implementation a normal ordered update.
if (_activatedControl == null)
OnMouseMoved(x, y);
}
else
{
// redundant - our parent handles this - but convenient for unit tests
ProcessMouseLeave(x, y);
}
}
/// <summary>Called when the mouse wheel has been rotated</summary>
/// <param name="ticks">Number of ticks that the mouse wheel has been rotated</param>
internal void ProcessMouseWheel(float ticks)
{
// If the mouse is being held down on a control, give it any mouse wheel
// messages. This enables some exotic uses for the mouse wheel, such as holding
// an object with the mouse button and scaling it with the wheel at the same time.
if (_activatedControl != null)
{
if (_activatedControl != this)
{
_activatedControl.ProcessMouseWheel(ticks);
return;
}
}
// If the mouse wheel has been used normally, send the wheel notifications to
// the control the mouse is over.
if (_mouseOverControl != null)
{
if (_mouseOverControl != this)
{
_mouseOverControl.ProcessMouseWheel(ticks);
return;
}
}
// We're the control the mouse is over, let the user code handle
// the mouse wheel rotation
OnMouseWheel(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>
/// <param name="repetition">Whether the key press is due to the user holding down a key</param>
internal bool ProcessKeyPress(Keys keyCode, bool repetition)
{
// If there's an activated control (one being held down by the mouse or having
// accepted a previous key press), this control will get the key press delivered,
// whether it wants to or not. We don't want to track for each key which control
// is currently processing it. ;-)
if (_activatedControl != null)
{
if (!repetition)
++_heldKeyCount;
// If one of our children is the activated control, pass on the message
if (_activatedControl != this)
_activatedControl.ProcessKeyPress(keyCode, repetition);
else
OnKeyPressed(keyCode); // We're the activated control
return true; // Ignore user code and always accept the key press
}
// A key has been pressed but no control is activated currently. This means we
// have to look for a control which feels responsible for the key press, starting
// with ourselves.
// Does the user code in our derived class feel responsible for this key?
// If so, we're the new activated control and the key has been handled.
if (OnKeyPressed(keyCode))
{
_activatedControl = this;
++_heldKeyCount;
return true;
}
// Nope, we have to ask our children (and they, potentially recursively, theirs)
// to find a control that feels responsible.
var encounteredOrderingControl = false;
for (var index = 0; index < _children.Count; ++index)
{
var child = _children[index];
// We only process one child that has the affectsOrdering field set. This
// ensures that key presses will not be delivered to windows sitting behind
// another window. Other siblings that are not windows are asked still.
if (child._affectsOrdering)
{
if (encounteredOrderingControl)
continue;
else
encounteredOrderingControl = true;
}
// Does this child feel responsible for the key press?
if (child.ProcessKeyPress(keyCode, repetition))
{
_activatedControl = child;
++_heldKeyCount;
return true;
}
}
// Neither we nor any of our children felt responsible for the key. Give up.
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>
internal void ProcessKeyRelease(Keys keyCode)
{
// Any key release should have an associated key press, otherwise, someone
// delivered notifications to us we should not have received.
Debug.Assert(
_heldKeyCount > 0,
"ProcessKeyRelease() called more often then ProcessKeyPress(); " +
"ProcessKeyRelease() was called more often the ProcessKeyPress() has been " +
"called with the repetition parameter set to false"
);
// If we receive a release, we must have a control on which the mouse
// was pressed (possibly even ourselves)
Debug.Assert(
_activatedControl != null,
"ProcessKeyRelease() had no control a key was pressed on; " +
"ProcessKeyRelease() was called on a control instance, but the control " +
"did not register a prior key press for itself or any of its child controls"
);
--_heldKeyCount;
if (_activatedControl != this)
_activatedControl.ProcessKeyRelease(keyCode);
else
OnKeyReleased(keyCode);
// If no more keys buttons are being held down, clear the activated control
if (!AnyKeysOrButtonsPressed)
_activatedControl = null;
}
/// <summary>Switches the mouse over control to a different control</summary>
/// <param name="newMouseOverControl">New control the mouse is hovering over</param>
private void SwitchMouseOverControl(GuiControl newMouseOverControl, float x, float y)
{
if (_mouseOverControl != newMouseOverControl)
{
// Tell the previous mouse-over control that the mouse is no longer
// hovering over it
if (_mouseOverControl != null)
_mouseOverControl.ProcessMouseLeave(x, y);
_mouseOverControl = newMouseOverControl;
// Inform the new mouse-over control that the mouse is now over it
newMouseOverControl.OnMouseEntered();
}
}
}
}