#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2010 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.ObjectModel;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Nuclex.Input;
using Nuclex.Support.Collections;
using Nuclex.UserInterface.Input;
namespace Nuclex.UserInterface.Controls {
/// Represents an element in the user interface
///
///
/// 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.
///
///
/// 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.
///
///
public partial class Control {
/// Initializes a new control
public Control() : this(false) { }
/// Initializes a new control
///
/// Whether the control comes to the top of the hierarchy when clicked
///
///
///
/// The 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.
///
///
/// 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 ;-)
///
///
protected Control(bool affectsOrdering) {
this.affectsOrdering = affectsOrdering;
this.children = new ParentingControlCollection(this);
}
/// Children of the control
public Collection Children {
get { return this.children; }
}
///
/// True if clicking the control or its children moves the control into
/// the foreground of the drawing hierarchy
///
public bool AffectsOrdering {
get { return this.affectsOrdering; }
}
/// Parent control this control is contained in
///
/// 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.
///
public Control Parent {
get { return this.parent; }
}
/// Name that can be used to uniquely identify the control
///
/// 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.
///
public string Name {
get { return this.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 != this.name) {
// Look for name collisions with our siblings
Control parent = Parent;
if(parent != null)
if(parent.children.IsNameTaken(value))
throw new DuplicateNameException("Another control is already using this name");
// Everything seems to be ok, accept the new name
this.name = value;
}
}
}
/// Moves the control into the foreground
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.
Control control = this;
while(!ReferenceEquals(control.parent, null)) {
ParentingControlCollection siblings = control.parent.children;
siblings.MoveToStart(siblings.IndexOf(control));
control = control.parent;
}
}
///
/// Obtains the absolute boundaries of the control in screen coordinates
///
/// The control's absolute screen coordinate boundaries
///
/// 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.
///
public RectangleF GetAbsoluteBounds() {
// Is this the topmost control in the hierarchy (the desktop control)?
if(ReferenceEquals(this.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(this.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 this.Bounds.ToOffset(this.screen.Width, this.screen.Height);
} else { // 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)
RectangleF parentBounds = this.parent.GetAbsoluteBounds();
// Determine the controls absolute position based on the absolute
// dimensions and position of the parent control
RectangleF controlBounds = this.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;
}
}
/// Called when an input command was sent to the control
/// Input command that has been triggered
/// Whether the command has been processed by the control
protected virtual bool OnCommand(Command command) { return false; }
/// Called when a button on the gamepad has been pressed
/// Button that has been pressed
///
/// True if the button press was handled by the control, otherwise false.
///
///
/// If the control indicates that it didn't handle the key press, it will not
/// receive the associated key release notification.
///
protected virtual bool OnButtonPressed(Buttons button) { return false; }
/// Called when a button on the gamepad has been released
/// Button that has been released
protected virtual void OnButtonReleased(Buttons button) { }
/// Called when the mouse position is updated
/// X coordinate of the mouse cursor on the control
/// Y coordinate of the mouse cursor on the control
protected virtual void OnMouseMoved(float x, float y) { }
/// Called when a mouse button has been pressed down
/// Index of the button that has been pressed
/// Whether the control has processed the mouse press
///
/// 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.
///
protected virtual void OnMousePressed(MouseButtons button) { }
/// Called when a mouse button has been released again
/// Index of the button that has been released
protected virtual void OnMouseReleased(MouseButtons button) { }
///
/// Called when the mouse has left the control and is no longer hovering over it
///
protected virtual void OnMouseLeft() { }
///
/// Called when the mouse has entered the control and is now hovering over it
///
protected virtual void OnMouseEntered() { }
/// Called when the mouse wheel has been rotated
/// Number of ticks that the mouse wheel has been rotated
protected virtual void OnMouseWheel(float ticks) { }
/// Called when a key on the keyboard has been pressed down
/// Code of the key that was pressed
///
/// True if the key press was handled by the control, otherwise false.
///
///
/// 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.
///
protected virtual bool OnKeyPressed(Keys keyCode) { return false; }
/// Called when a key on the keyboard has been released again
/// Code of the key that was released
protected virtual void OnKeyReleased(Keys keyCode) { }
/// GUI instance this control belongs to. Can be null.
internal Screen Screen {
get { return this.screen; }
}
/// Called when a command was sent to the control
/// Command to be injected
/// Whether the command has been processed
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");
}
}
}
/// Assigns a new parent to the control
/// New parent to assign to the control
internal void SetParent(Control parent) {
this.parent = parent;
// Have we been assigned to a parent?
if(this.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(this.screen, parent.screen))
SetScreen(parent.screen);
} else { // No parent, we're now officially an orphan ;)
// Orphans don't have screens!
SetScreen(null);
}
}
/// Assigns a new GUI to the control
/// New GUI to assign to the control
internal void SetScreen(Screen gui) {
this.screen = gui;
this.children.SetScreen(gui);
}
/// Control the mouse is currently over
internal protected Control MouseOverControl {
get { return this.mouseOverControl; }
}
/// Control that currently captured incoming input
internal protected Control ActivatedControl {
get { return this.activatedControl; }
}
/// Location and extents of the control
public UniRectangle Bounds;
/// Control this control is contained in
private Control parent;
/// GUI instance this control has been added to. Can be null.
private Screen screen;
/// Name of the control instance (for programmatic identification)
private string name;
/// Whether this control can obtain the input focus
private bool affectsOrdering;
/// Child controls belonging to this control
///
/// 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.
///
private ParentingControlCollection children;
}
} // namespace Nuclex.UserInterface.Controls