#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Nuclex.Input;
using Nuclex.Support;
using Nuclex.UserInterface.Input;
using Nuclex.UserInterface.Controls;
namespace Nuclex.UserInterface {
/// Manages the controls and their state on a GUI screen
///
/// This class manages the global state of a distinct user interface. Unlike your
/// typical GUI library, the Nuclex.UserInterface library can handle any number of
/// simultaneously active user interfaces at the same time, making the library
/// suitable for usage on virtual ingame computers and multi-client environments
/// such as split-screen games or switchable graphical terminals.
///
public class Screen : IInputReceiver {
/// Triggered when the control in focus changes
public event EventHandler FocusChanged;
/// Initializes a new GUI
public Screen() : this(0, 0) { }
/// Initializes a new GUI
/// Width of the area the GUI can occupy
/// Height of the area the GUI can occupy
///
/// Width and height should reflect the entire drawable area of your GUI. If you
/// want to limit the region which the GUI is allowed to use (eg. to only use the
/// safe area of a TV) please resize the desktop control accordingly!
///
public Screen(float width, float height) {
this.Width = width;
this.Height = height;
this.heldKeys = new BitArray(maxKeyboardKey + 1);
this.heldButtons = 0;
// By default, the desktop control will cover the whole drawing area
this.desktopControl = new DesktopControl();
this.desktopControl.Bounds.Size.X.Fraction = 1.0f;
this.desktopControl.Bounds.Size.Y.Fraction = 1.0f;
this.desktopControl.SetScreen(this);
this.focusedControl = new WeakReference(null);
}
/// Width of the screen in pixels
public float Width {
get { return this.size.X; }
set { this.size.X = value; }
}
/// Height of the screen in pixels
public float Height {
get { return this.size.Y; }
set { this.size.Y = value; }
}
/// Control responsible for hosting the GUI's top-level controls
public Control Desktop {
get { return this.desktopControl; }
}
/// Injects a command into the processor
/// Input command that will be injected
public void InjectCommand(Command command) {
switch(command) {
// Accept or cancel the current control
case Command.Accept:
case Command.Cancel: {
Control focusedControl = FocusedControl;
if(focusedControl == null) {
return; // Also catches when focusedControl is not part of the tree
}
// TODO: Should this be propagated down the control tree?
focusedControl.ProcessCommand(command);
break;
}
// Change focus to another control
case Command.SelectPrevious:
case Command.SelectNext: {
// TODO: Implement focus switching
break;
}
// Control specific. Changes focus if unhandled.
case Command.Up:
case Command.Down:
case Command.Left:
case Command.Right: {
Control focusedControl = FocusedControl;
if(focusedControl == null) {
return; // Also catches when focusedControl is not part of the tree
}
// First send the command to the focused control. If the control handles
// the command, there's nothing for us to do. Otherwise, use the directional
// commands for focus switching.
if(focusedControl.ProcessCommand(command)) {
return;
}
// These will be determined in the following code block
float nearestDistance = float.NaN;
Control nearestControl = null;
{
// Determine the center of the focused control
RectangleF parentBounds = focusedControl.Parent.GetAbsoluteBounds();
RectangleF focusedBounds = focusedControl.Bounds.ToOffset(
parentBounds.Width, parentBounds.Height
);
// Search all siblings of the focused control for the nearest control in the
// direction the command asks to move into
Collection siblings = focusedControl.Parent.Children;
for(int index = 0; index < siblings.Count; ++index) {
Control sibling = siblings[index];
// Only consider this sibling if it's focusable
if(!ReferenceEquals(sibling, focusedControl) && canControlGetFocus(sibling)) {
RectangleF siblingBounds = sibling.Bounds.ToOffset(
parentBounds.Width, parentBounds.Height
);
// Calculate the distance the control has in the direction focus is being
// changed to. If the control doesn't lie in that direction, NaN will
// be returned
float distance = getDirectionalDistance(
ref focusedBounds, ref siblingBounds, command
);
if(float.IsNaN(nearestDistance) || (distance < nearestDistance)) {
nearestControl = sibling;
nearestDistance = distance;
}
}
}
} // beauty scope
// Search completed, if we found a candidate, change focus to it
if(nearestDistance != float.NaN) {
FocusedControl = nearestControl;
}
break;
}
}
}
/// Called when a key on the keyboard has been pressed down
/// Code of the key that was pressed
public void InjectKeyPress(Keys keyCode) {
bool repetition = this.heldKeys.Get((int)keyCode);
// If a control is activated, it will receive any input notifications
if(this.activatedControl != null) {
// The desktop control might still reject a key press if the activated
// control was closed by the previous key press and thus, while the screen
// has an activated control, the desktop control no longer has.
if(this.activatedControl.ProcessKeyPress(keyCode, repetition)) {
if(!repetition) {
++this.heldKeyCount;
this.heldKeys.Set((int)keyCode, true);
}
}
return;
}
// No control is activated, try the focused control before searching
// the entire tree for a responder.
Control focusedControl = this.focusedControl.Target;
if(focusedControl != null) {
if(focusedControl.ProcessKeyPress(keyCode, false)) {
this.activatedControl = focusedControl;
if(!repetition) {
++this.heldKeyCount;
this.heldKeys.Set((int)keyCode, true);
}
return;
}
}
// Focused control didn't process the notification, now let the desktop
// control traverse the entire control tree is earch for a handler.
if(this.desktopControl.ProcessKeyPress(keyCode, false)) {
this.activatedControl = this.desktopControl;
if(!repetition) {
++this.heldKeyCount;
this.heldKeys.Set((int)keyCode, true);
}
} else {
switch(keyCode) {
case Keys.Up: { InjectCommand(Command.Up); break; }
case Keys.Down: { InjectCommand(Command.Down); break; }
case Keys.Left: { InjectCommand(Command.Left); break; }
case Keys.Right: { InjectCommand(Command.Right); break; }
case Keys.Enter: { InjectCommand(Command.Accept); break; }
case Keys.Escape: { InjectCommand(Command.Cancel); break; }
}
}
}
/// Called when a key on the keyboard has been released again
/// Code of the key that was released
public void InjectKeyRelease(Keys keyCode) {
if(!this.heldKeys.Get((int)keyCode)) {
return;
}
--this.heldKeyCount;
this.heldKeys.Set((int)keyCode, false);
// If a control signed responsible for the earlier key press, it will now
// receive the release notification.
if(this.activatedControl != null) {
this.activatedControl.ProcessKeyRelease(keyCode);
}
// Reset the activated control if the user has released all buttons on all
// input devices.
if(!anyKeysOrButtonsPressed) {
this.activatedControl = null;
}
}
/// Handle user text input by a physical or virtual keyboard
/// Character that has been entered
public void InjectCharacter(char character) {
// Send the text to the currently focused control in the GUI
Control focusedControl = this.focusedControl.Target;
IWritable writable = focusedControl as IWritable;
if(writable != null) {
writable.OnCharacterEntered(character);
}
}
/// Called when a button on the gamepad has been pressed
/// Button that has been pressed
public void InjectButtonPress(Buttons button) {
Buttons newHeldButtons = this.heldButtons | button;
if(newHeldButtons == this.heldButtons) {
return;
}
this.heldButtons = newHeldButtons;
// If a control is activated, it will receive any input notifications
if(this.activatedControl != null) {
this.activatedControl.ProcessButtonPress(button);
return;
}
// No control is activated, try the focused control before searching
// the entire tree for a responder.
Control focusedControl = this.focusedControl.Target;
if(focusedControl != null) {
if(focusedControl.ProcessButtonPress(button)) {
this.activatedControl = focusedControl;
return;
}
}
// Focused control didn't process the notification, now let the desktop
// control traverse the entire control tree is earch for a handler.
if(this.desktopControl.ProcessButtonPress(button)) {
this.activatedControl = this.desktopControl;
}
}
/// Called when a button on the gamepad has been released
/// Button that has been released
public void InjectButtonRelease(Buttons button) {
if((this.heldButtons & button) == 0) {
return;
}
this.heldButtons &= ~button;
// If a control signed responsible for the earlier button press, it will now
// receive the release notification.
if(this.activatedControl != null) {
this.activatedControl.ProcessButtonRelease(button);
}
// Reset the activated control if the user has released all buttons on all
// input devices.
if(!anyKeysOrButtonsPressed) {
this.activatedControl = null;
}
}
/// Injects a mouse position update into the GUI
/// X coordinate of the mouse cursor within the screen
/// Y coordinate of the mouse cursor within the screen
public void InjectMouseMove(float x, float y) {
this.desktopControl.ProcessMouseMove(this.size.X, this.size.Y, x, y);
}
/// Called when a mouse button has been pressed down
/// Index of the button that has been pressed
public void InjectMousePress(MouseButtons button) {
this.heldMouseButtons |= button;
// If a control is activated, it will receive any input notifications
if(this.activatedControl != null) {
this.activatedControl.ProcessMousePress(button);
return;
}
// No control was activated, so the desktop control becomes activated and
// is responsible for routing the input to the control under the mouse.
if(this.desktopControl.ProcessMousePress(button)) {
this.activatedControl = this.desktopControl;
}
}
/// Called when a mouse button has been released again
/// Index of the button that has been released
public void InjectMouseRelease(MouseButtons button) {
this.heldMouseButtons &= ~button;
// If a control signed responsible for the earlier mouse press, it will now
// receive the release notification.
if(this.activatedControl != null) {
this.activatedControl.ProcessMouseRelease(button);
}
// Reset the activated control if the user has released all buttons on all
// input devices.
if(!anyKeysOrButtonsPressed) {
this.activatedControl = null;
}
}
/// Called when the mouse wheel has been rotated
/// Number of ticks that the mouse wheel has been rotated
public void InjectMouseWheel(float ticks) {
if(this.activatedControl != null) {
this.activatedControl.ProcessMouseWheel(ticks);
} else {
this.desktopControl.ProcessMouseWheel(ticks);
}
}
/// Triggers the FocusChanged event
/// Control that has gotten the input focus
private void onFocusChanged(Control focusedControl) {
if(FocusChanged != null) {
FocusChanged(this, new ControlEventArgs(focusedControl));
}
}
/// Whether the GUI has currently captured the input devices
///
///
/// When you mix GUIs and gameplay (for example, in a strategy game where the GUI
/// manages the build menu and the remainder of the screen belongs to the game),
/// it is important to keep control of who currently owns the input devices.
///
///
/// Assume the player is drawing a selection rectangle around some units using
/// the mouse. He will press the mouse button outside any GUI elements, keep
/// holding it down and possibly drag over the GUI. Until the player lets go
/// of the mouse button, input exclusively belongs to the game. The same goes
/// vice versa, of course.
///
///
/// This property tells whether the GUI currently thinks that all input belongs
/// to it. If it is true, the game should not process any input. The GUI will
/// implement the input model as described here and respect the game's ownership
/// of the input devices if a mouse button is pressed outside of the GUI. To
/// correctly handle input device ownership, send all input to the GUI
/// regardless of this property's value, then check this property and if it
/// returns false let your game process the input.
///
///
public bool IsInputCaptured {
get { return this.desktopControl.IsInputCaptured; }
}
/// True if the mouse is currently hovering over any GUI elements
///
/// Useful if you mix gameplay with a GUI and use different mouse cursors
/// depending on the location of the mouse. As long as input is not captured
/// (see ) you can use this property to know
/// whether you should use the standard GUI mouse cursor or let your game
/// decide which cursor to use.
///
public bool IsMouseOverGui {
get { return this.desktopControl.IsMouseOverGui; }
}
/// Child control that currently has the input focus
public Control FocusedControl {
get {
Control current = this.focusedControl.Target;
if((current != null) && ReferenceEquals(current.Screen, this)) {
return current;
} else {
return null;
}
}
set {
Control current = this.focusedControl.Target;
if(!ReferenceEquals(value, current)) {
this.focusedControl.Target = value;
onFocusChanged(value);
}
}
}
///
/// Whether any keys, mouse buttons or game pad buttons are beind held pressed
///
private bool anyKeysOrButtonsPressed {
get {
return
(this.heldMouseButtons != 0) ||
(this.heldKeyCount > 0) ||
(this.heldButtons != 0);
}
}
///
/// Determines the distance of one rectangle to the other, also taking direction
/// into account
///
/// Boundaries of the base rectangle
/// Boundaries of the other rectangle
/// Direction into which distance will be determined
///
/// The direction of the other rectangle of NaN if it didn't lie in that direction
///
private static float getDirectionalDistance(
ref RectangleF ownBounds, ref RectangleF otherBounds, Command direction
) {
float closestPointX, closestPointY;
float distance;
bool isVertical =
(direction == Command.Up) ||
(direction == Command.Down);
if(isVertical) {
float ownCenterX = ownBounds.X + (ownBounds.Width / 2.0f);
// Take an imaginary line through the other control's center, perpendicular
// to the specified direction. Then locate the closest point on that line
// to our own center.
closestPointX = Math.Min(Math.Max(ownCenterX, otherBounds.Left), otherBounds.Right);
closestPointY = otherBounds.Y + (otherBounds.Height / 2.0f);
// Find out whether we need to check the diagonal quadrant boundary
bool leavesLeft = (closestPointX < ownBounds.Left);
bool leavesRight = (closestPointX > ownBounds.Right);
//
float sideY;
if(direction == Command.Up) {
sideY = ownBounds.Top;
if((closestPointY > sideY) && (leavesLeft || leavesRight)) {
return float.NaN;
}
distance = sideY - closestPointY;
} else {
sideY = ownBounds.Bottom;
if((closestPointY < sideY) && (leavesLeft || leavesRight)) {
return float.NaN;
}
distance = closestPointY - sideY;
}
float distanceY = Math.Abs(sideY - closestPointY);
if(leavesLeft) {
float distanceX = Math.Abs(ownBounds.Left - closestPointX);
if(distanceX > distanceY) {
return float.NaN;
}
} else if(leavesRight) {
float distanceX = Math.Abs(closestPointX - ownBounds.Right);
if(distanceX > distanceY) {
return float.NaN;
}
}
} else {
float ownCenterY = ownBounds.Y + (ownBounds.Height / 2.0f);
// Take an imaginary line through the other control's center, perpendicular
// to the specified direction. Then locate the closest point on that line
// to our own center.
closestPointX = otherBounds.X + (otherBounds.Width / 2.0f);
closestPointY = Math.Min(Math.Max(ownCenterY, otherBounds.Top), otherBounds.Bottom);
// Find out whether we need to check the diagonal quadrant boundary
bool leavesTop = (closestPointY < ownBounds.Top);
bool leavesBottom = (closestPointY > ownBounds.Bottom);
float sideX;
if(direction == Command.Left) {
sideX = ownBounds.Left;
if((closestPointX > sideX) && (leavesTop || leavesBottom)) {
return float.NaN;
}
distance = sideX - closestPointX;
} else {
sideX = ownBounds.Right;
if((closestPointX < sideX) && (leavesTop || leavesBottom)) {
return float.NaN;
}
distance = closestPointX - sideX;
}
float distanceX = Math.Abs(sideX - closestPointX);
if(leavesTop) {
float distanceY = Math.Abs(ownBounds.Top - closestPointY);
if(distanceY > distanceX) {
return float.NaN;
}
} else if(leavesBottom) {
float distanceY = Math.Abs(closestPointY - ownBounds.Bottom);
if(distanceY > distanceX) {
return float.NaN;
}
}
}
return (distance < 0.0f) ? float.NaN : distance;
}
/// Determines whether a control can obtain the input focus
/// Control that will be checked for focusability
/// True if the specified control can obtain the input focus
private static bool canControlGetFocus(Control control) {
IFocusable focusableControl = control as IFocusable;
if(focusableControl != null) {
return focusableControl.CanGetFocus;
} else {
return false;
}
}
/// Highest value in the Keys enumeration
private static readonly int maxKeyboardKey =
(int)EnumHelper.GetHighestValue();
/// Size of the GUI area in world units or pixels
private Vector2 size;
/// Control responsible for hosting the GUI's top-level controls
private DesktopControl desktopControl;
/// Child that currently has the input focus
///
/// If this field is non-null, all keyboard input sent to the Gui is handed
/// over to the focused control. Otherwise, keyboard input is discarded.
///
private WeakReference focusedControl;
/// Control the user has activated through one of the input devices
private Control activatedControl;
/// Number of keys being held down on the keyboard
private int heldKeyCount;
/// Keys on the keyboard the user is currently holding down
BitArray heldKeys;
/// Buttons on the game pad the user is currently holding down
Buttons heldButtons;
/// Mouse buttons currently being held down
private MouseButtons heldMouseButtons;
}
} // namespace Nuclex.UserInterface