#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