#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 { partial class Control { /// Called when a button on the game pad has been pressed /// Button that has been pressed /// /// 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 /// 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(this.activatedControl != null) { ++this.heldButtonCount; // If one of our children is the activated control, pass on the message if(this.activatedControl != this) { this.activatedControl.ProcessButtonPress(button); } else { // We're the activated control 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)) { this.activatedControl = this; ++this.heldButtonCount; return true; } // Nope, we have to ask our children to find a control that feels responsible. bool encounteredOrderingControl = false; for(int index = 0; index < this.children.Count; ++index) { Control child = this.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, so // a bunch of buttons on the desktop would be asked in addition to a window. if(child.affectsOrdering) { if(encounteredOrderingControl) { continue; } else { encounteredOrderingControl = true; } } // Does this child feel responsible for the button press? if(child.ProcessButtonPress(button)) { this.activatedControl = child; ++this.heldButtonCount; return true; } } // Neither we nor any of our children felt responsible for the button. Give up. return false; } /// Called when a button on the game pad has been released /// Button that has been released 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(this.heldButtonCount == 0) { return; } // If we receive a release, we must have a control on which the mouse // was pressed (possibly even ourselves) Debug.Assert( this.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" ); --this.heldButtonCount; if(this.activatedControl != this) { this.activatedControl.ProcessButtonRelease(button); } else { OnButtonReleased(button); } // If no more keys buttons are being held down, clear the activated control if(!anyKeysOrButtonsPressed) { this.activatedControl = null; } } /// /// Called when the mouse has left the control and is no longer hovering over it /// internal void ProcessMouseLeave() { // 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(this.mouseOverControl != null) { if(this.mouseOverControl != this) { this.mouseOverControl.ProcessMouseLeave(); } else { OnMouseLeft(); } this.mouseOverControl = null; } } /// 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 internal bool ProcessMousePress(MouseButtons 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(this.activatedControl == null) { this.activatedControl = this.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(this.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(this.activatedControl != this) { if(this.activatedControl.affectsOrdering) { this.children.MoveToStart(this.children.IndexOf(this.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. this.heldMouseButtons |= button; // If the mouse is over another control, pass on the mouse press. if(this.activatedControl != this) { return this.activatedControl.ProcessMousePress(button); } else { // Otherwise, the mouse press applies to us // If this control can take the input focus, make it the focused control if(this.screen != null) { IFocusable focusable = this as IFocusable; if((focusable != null) && (focusable.CanGetFocus)) { this.screen.FocusedControl = this; } } // Deliver the notification to the control deriving from us OnMousePressed(button); return true; } } /// Called when a mouse button has been released again /// Index of the button that has been released internal void ProcessMouseRelease(MouseButtons 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((this.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( this.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. this.heldMouseButtons &= ~button; // If the mouse was held over one of our childs, pass on the notification if(this.activatedControl != this) { this.activatedControl.ProcessMouseRelease(button); } else { OnMouseReleased(button); } // If no more mouse buttons are being held down, clear the mouse-press control if(!anyKeysOrButtonsPressed) { this.activatedControl = null; } } /// Processes mouse movement notifications /// Absolute width of the control's container /// Absolute height of the control's container /// Absolute X position of the mouse within the container /// Absolute Y position of the mouse within the container internal void ProcessMouseMove( float containerWidth, float containerHeight, float x, float y ) { // Calculate the absolute pixel position and size of this control Vector2 size = this.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(this.activatedControl != null) { float mouseX = x - this.Bounds.Location.X.ToOffset(containerWidth); float mouseY = y - this.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(this.activatedControl != this) { 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 -= this.Bounds.Location.X.ToOffset(containerWidth); y -= this.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. for(int index = 0; index < this.children.Count; ++index) { RectangleF childBounds = this.children[index].Bounds.ToOffset(size.X, size.Y); // Is the mouse over this child? if(childBounds.Contains(x, y)) { switchMouseOverControl(this.children[index]); // 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(this.mouseOverControl != this.activatedControl) { this.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); // 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(this.activatedControl == null) { OnMouseMoved(x, y); } } else { // redundant - our parent handles this - but convenient for unit tests ProcessMouseLeave(); } } /// Called when the mouse wheel has been rotated /// Number of ticks that the mouse wheel has been rotated 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(this.activatedControl != null) { if(this.activatedControl != this) { 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(this.mouseOverControl != null) { if(this.mouseOverControl != this) { this.mouseOverControl.ProcessMouseWheel(ticks); return; } } // We're the control the mouse is over, let the user code handle // the mouse wheel rotation OnMouseWheel(ticks); } /// Called when a key on the keyboard has been pressed down /// Code of the key that was pressed /// /// Whether the key press is due to the user holding down a key /// 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(this.activatedControl != null) { if(!repetition) { ++this.heldKeyCount; } // If one of our children is the activated control, pass on the message if(this.activatedControl != this) { this.activatedControl.ProcessKeyPress(keyCode, repetition); } else { // We're the activated control OnKeyPressed(keyCode); } 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)) { this.activatedControl = this; ++this.heldKeyCount; return true; } // Nope, we have to ask our children (and they, potentially recursively, theirs) // to find a control that feels responsible. bool encounteredOrderingControl = false; for(int index = 0; index < this.children.Count; ++index) { Control child = this.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)) { this.activatedControl = child; ++this.heldKeyCount; return true; } } // Neither we nor any of our children felt responsible for the key. Give up. return false; } /// Called when a key on the keyboard has been released again /// Code of the key that was released 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( this.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( this.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" ); --this.heldKeyCount; if(this.activatedControl != this) { this.activatedControl.ProcessKeyRelease(keyCode); } else { OnKeyReleased(keyCode); } // If no more keys buttons are being held down, clear the activated control if(!anyKeysOrButtonsPressed) { this.activatedControl = null; } } /// /// 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.heldButtonCount > 0); } } /// Switches the mouse over control to a different control /// New control the mouse is hovering over private void switchMouseOverControl(Control newMouseOverControl) { if(this.mouseOverControl != newMouseOverControl) { // Tell the previous mouse-over control that the mouse is no longer // hovering over it if(this.mouseOverControl != null) { this.mouseOverControl.ProcessMouseLeave(); } this.mouseOverControl = newMouseOverControl; // Inform the new mouse-over control that the mouse is now over it newMouseOverControl.OnMouseEntered(); } } /// Mouse buttons the user is holding down over the control private MouseButtons heldMouseButtons; /// Number of keyboard keys being held down private int heldKeyCount; /// Number of game pad buttons being held down private int heldButtonCount; /// Control the mouse is currently hovering over private Control mouseOverControl; /// Control the mouse was pressed down on private Control activatedControl; } } // namespace Nuclex.UserInterface.Controls