using System; using System.Timers; using Chernobyl.Collections.Generic; using Chernobyl.Collections.Generic.Event; using Chernobyl.Creation; using Chernobyl.Dependency; using Chernobyl.DesignPatterns.Extension; using Chernobyl.Event; using Chernobyl.Graphics; using Chernobyl.Graphics.Drawing; using Chernobyl.Graphics.Polygon; using Chernobyl.Graphics.Polygon.Buffers; using Chernobyl.Graphics.Writing; using Chernobyl.Input.Controls; using Chernobyl.Mathematics; using Chernobyl.Mathematics.Geometry; using Chernobyl.Mathematics.Movement; using Chernobyl.Mathematics.Vectors; using Chernobyl.Switch; using Chernobyl.Utility; namespace Chernobyl.Interface.Writing { /// /// Places a text cursor (aka, a caret) on an instance /// and allows that cursor to be moved using the keyboard keys. /// public class TextCursor : Extension { /// /// Initializes a new instance of the class. /// /// The /// instance that takes and gives out services. public TextCursor(IEventCollection services) : this(services, null) { } /// /// Initializes a new instance of the class and /// attaches this to the passed in /// . /// /// The /// instance that takes and gives out services. /// The to attach to or null /// if no should be attached to right now. public TextCursor(IEventCollection services, IText extended) { _cursorFlashRateInMilliseconds = DefaultCursorFlashRateInMilliseconds; Services = services; LeftRepeater = new Repeater(100); RightRepeater = new Repeater(100); Extended = extended; } /// /// An event handler that can be assigned to an event. This method moves /// this text cursor left. By default, it is assigned to a /// which is attached to the left arrow keyboard /// key. /// /// The sender. /// The instance containing /// the event data. public void MoveLeft(object sender, EventArgs e) { if(IndexLocation != Extended.MinimumDrawIndex) IndexLocation--; } /// /// An event handler that can be assigned to an event. This method moves /// this text cursor right. By default, it is assigned to a /// which is attached to the right arrow keyboard /// key. /// /// The sender. /// The instance containing /// the event data. public void MoveRight(object sender, EventArgs e) { if (IndexLocation != Extended.Writing.Length) IndexLocation++; } /// /// An event handler that can be assigned to an event. This method moves /// this text cursor to the beginning of the text. By default, it is /// assigned to the home keyboard key. /// /// The sender. /// The instance containing /// the event data. public void MoveToBeginning(object sender, EventArgs e) { IndexLocation = 0; } /// /// An event handler that can be assigned to an event. This method moves /// this text cursor to the end of the text. By default, it is assigned /// to the end keyboard key. /// /// The sender. /// The instance containing /// the event data. public void MoveToEnd(object sender, EventArgs e) { IndexLocation = Extended.Writing.Length; } /// /// The index (zero based) location of the text cursor in the string of /// the attached . The cursor is placed right before /// this index location. /// public int IndexLocation { get { return _indexLocation; } set { if(IndexLocationChanged != null) { ValueChangedEventArgs vce = new ValueChangedEventArgs(_indexLocation, value); _indexLocation = value; TextCursorRender.IsUpdateNeeded = true; IndexLocationChanged(this, vce); } else { _indexLocation = value; TextCursorRender.IsUpdateNeeded = true; } } } /// /// An event that is raised after the /// property has changed. /// public event EventHandler> IndexLocationChanged; /// /// The instance that this /// gets it's keyboard events from. /// [Inject] public IKeyboard Keyboard { get { return _keyboard; } set { // if we have a previous keyboard we are going to need to un-assign // ourself from it's events and assign ourself to the events from // the newest keyboard. Otherwise, we will wait till we've // attached to an IText before we assign ourselves to keyboard // events if (Extended != null) { if (_keyboard != null) UnassignFromKeyboardEvents(); _keyboard = value; AssignToKeyboardEvents(); } else _keyboard = value; } } /// /// The used as the default Chernobyl /// for rendering. /// [Inject] public Scene DefaultScene { set { _sceneDrawLocation = value.Draw2D; // If the DrawBuffer is ready and does not have a parent, parent // it. if (DrawBuffer != null && DrawBuffer.DrawableParent == null) DrawBuffer.DrawableParent = _sceneDrawLocation; } } /// /// The amount of time (in milliseconds) it takes for the cursor to flash /// on and off. /// /// The default value for this property is the value of the /// field. public static int CursorFlashRateInMilliseconds { get { return _cursorFlashRateInMilliseconds; } set { _cursorFlashRateInMilliseconds = value; // if the flashing timer has already been created, we need to // recreate it if (FlashingTimer == null) FlashingTimer = new Timer(_cursorFlashRateInMilliseconds); } } /// /// The default value of the /// property. /// public static int DefaultCursorFlashRateInMilliseconds = 600; /// /// Attaches this extension to the passed in object so that is can be /// extended. /// /// The object to extend. protected override void AttachTo(IText extended) { extended.Extensions.Add(this); extended.WritingChanged += extended_WritingChanged; extended.MinimumDrawIndexChanged += extended_MinimumDrawIndexChanged; extended.MaximumDrawLengthChanged += extended_MaximumDrawLengthChanged; if (TextCursorDrawParent == null) { Services.Create, IBuffer>>( (sender, e) => DrawBuffer = e.CreatedInstance, new Pair, IBuffer>(Services, VertexColor.CreateSquare(Services, ColorRgba.Black))); // If the DrawBuffer is ready and does not have a parent, parent // it. if (_sceneDrawLocation != null && DrawBuffer != null && DrawBuffer.DrawableParent == null) DrawBuffer.DrawableParent = _sceneDrawLocation; // create the flashing timer, the "set" of the FlashingTimer // property will properly configure the flashing timer and // OnOffDrawable if (FlashingTimer == null) FlashingTimer = new Timer(CursorFlashRateInMilliseconds); } // create a TransformRectangle of 1x1. The transform rectangle will // automatically adjust as we scale the sprite. Currently, the sprite // is using a 1x1 square mesh element to render textures to, therefore, // a 1x1 TransformRectangle will only work properly with a 1x1 mesh // element square. Rectangle rectangle = new Rectangle(-0.5f, -0.5f, 1, 1); Transform.MakeParentChild(extended, rectangle); // create a render for this instance TextCursorRender = new Render(Services, PrimitiveType.TriangleStrip, null, new TextCursorTransform(this), rectangle); TextCursorRender.DrawableParent = TextCursorDrawParent; Transform.MakeParentChild(extended, TextCursorRender); // scale the render so that it is the same height as the text we are // a cursor of. Also, place the cursor at the beginning of the line // of text by default TextCursorRender.ScaleY(extended.Height); IndexLocation = extended.Writing.Length; TextCursorRender.IsUpdateNeeded = true; AssignToKeyboardEvents(); } /// /// Removes the decorator from an object so that it is no longer extended. /// /// The object to remove the extension from. protected override void DetachFrom(IText extended) { TextCursorDrawParent.DrawableChildren.Remove(TextCursorRender); Transform.SeperateParentChild(extended, TextCursorRender); extended.Extensions.Remove(this); extended.WritingChanged -= extended_WritingChanged; UnassignFromKeyboardEvents(); } /// /// Un-assigns an instance of this class to the necessary events from the /// . /// void UnassignFromKeyboardEvents() { // setup the left arrow key Keyboard.Left.OnDown -= LeftRepeater.Start; Keyboard.Left.OnUp -= LeftRepeater.Stop; LeftRepeater.Elapsed -= MoveLeft; LeftRepeater.SwitchedOn -= MoveLeft; // setup the left arrow key Keyboard.Right.OnDown -= RightRepeater.Start; Keyboard.Right.OnUp -= RightRepeater.Stop; RightRepeater.Elapsed -= MoveRight; RightRepeater.SwitchedOn -= MoveRight; // setup the home and end keys Keyboard.Home.OnDown -= MoveToBeginning; Keyboard.End.OnDown -= MoveToEnd; } /// /// Assigns an instance of this class to the necessary events from the /// . /// void AssignToKeyboardEvents() { // setup the left arrow key Keyboard.Left.OnDown += LeftRepeater.Start; Keyboard.Left.OnUp += LeftRepeater.Stop; LeftRepeater.Elapsed += MoveLeft; LeftRepeater.SwitchedOn += MoveLeft; // setup the left arrow key Keyboard.Right.OnDown += RightRepeater.Start; Keyboard.Right.OnUp += RightRepeater.Stop; RightRepeater.Elapsed += MoveRight; RightRepeater.SwitchedOn += MoveRight; // setup the home and end keys Keyboard.Home.OnDown += MoveToBeginning; Keyboard.End.OnDown += MoveToEnd; } /// /// Handles the event on the attached /// instance. This method dirties the matrix of the /// so that it will recalculate it's position. /// /// The source of the event. /// The event data. void extended_WritingChanged(object sender, ValueChangedEventArgs e) { TextCursorRender.IsUpdateNeeded = true; } /// /// Handles the MaximumDrawLengthChanged event of the extended control. /// This method dirties the matrix of the /// so that it will recalculate it's position. /// /// The source of the event. /// The instance /// containing the event data. void extended_MaximumDrawLengthChanged(object sender, ValueChangedEventArgs e) { TextCursorRender.IsUpdateNeeded = true; } /// /// Handles the MinimumDrawIndexChanged event of the extended control. /// This method dirties the matrix of the /// so that it will recalculate it's position. /// /// The source of the event. /// The instance /// containing the event data. void extended_MinimumDrawIndexChanged(object sender, ValueChangedEventArgs e) { TextCursorRender.IsUpdateNeeded = true; } /// /// The instance used to render and transform the /// text cursor. /// IRender TextCursorRender { get; set; } /// /// The used to control the repeat rates of /// several keys. /// Repeater LeftRepeater { get; set; } /// /// The used to control the repeat rates of /// several keys. /// Repeater RightRepeater { get; set; } /// /// The that was given to this class in its /// constructor. When a new instance is set on /// this property the class will call /// /// passing in 'this' instance. /// IEventCollection Services { get { return _services; } set { _services = value; _services.Inject(this); } } /// /// The parent that the text cursor /// s attach to. /// static IDrawable TextCursorDrawParent { get { return OnOffDrawable; } } /// /// The instance that renders the cursor geometry to the screen. /// static IDrawBuffer DrawBuffer { get { return _drawBuffer; } set { value.ThrowIfNull("value"); IDrawBuffer previousValue = _drawBuffer; _drawBuffer = value; if(previousValue != null) { if (OnOffDrawable.DrawableParent == previousValue) OnOffDrawable.DrawableParent = null; } OnOffDrawable.DrawableParent = _drawBuffer; } } /// /// The used to turn off the /// cursor instances. /// static OnOffDrawable OnOffDrawable { get { return _onOffDrawable ?? (_onOffDrawable = new OnOffDrawable((IDrawable)null)); } } /// /// The timer that is used to generate the flashing cursor. Setting this /// property will cause the to no longer /// listen to the previous FlashingTimer's /// event and start listening to the event of the new instance. Also, /// the new timer will be started using . /// static Timer FlashingTimer { get { return _flashingTimer; } set { if (_flashingTimer != null) _flashingTimer.Elapsed -= OnOffDrawable.Toggle; _flashingTimer = value; _flashingTimer.Elapsed += OnOffDrawable.Toggle; _flashingTimer.Start(); } } /// /// An specifically made to move the cursor /// around some text. /// class TextCursorTransform : RemoveScaleTransform { /// /// Initializes a new instance of the /// class. /// /// The text cursor. public TextCursorTransform(TextCursor textCursor) { TextCursor = new WeakReference(textCursor); } /// /// Orders this transform so that it's world matrix represents it's /// local matrix relative to it's parent's world matrix. /// protected override void Update() { if (IsUpdateNeeded == true) { TextCursor textCursor = TextCursor.Target; IText text = textCursor.Extended; string writing = text.Writing; // if the location is past the end of the text, then we'll slap the // cursor on the end. Don't set the length using "IndexLocation" // though since the "set" of that property calls this method. if (textCursor.IndexLocation > writing.Length) textCursor.IndexLocation = writing.Length; // measure the distance from the beginning of the string to the // location of the cursor and then use that distance to get the // distance from the center of the string (transform) to the // cursor's location float stringWidth = (text.Font.MeasureString(writing.Substring(text.MinimumDrawIndex, textCursor.IndexLocation - text.MinimumDrawIndex))).X; float distanceFromCenter = stringWidth - (text.Width * 0.5f); // now place the reposition the transform of the cursor instance. Vector4 row3 = LocalMatrix.Row3; Matrix4 transform = LocalMatrix; transform.Row3 = new Vector4(distanceFromCenter, row3.Y, row3.Z, row3.W); SetTransformation(ref transform); base.Update(); } } /// /// The wrapped in a /// to prevent cyclical referencing (allows instances to be garbage /// collected). /// WeakReference TextCursor { get; set; } } /// /// The backing field to . /// static IDrawBuffer _drawBuffer; /// /// The backing field to . /// static OnOffDrawable _onOffDrawable; /// /// The backing field to . /// static int _cursorFlashRateInMilliseconds; /// /// The backing field to . /// static Timer _flashingTimer; /// /// The backing field to . /// IEventCollection _services; /// /// The backing field to . /// int _indexLocation; /// /// The backing field to . /// IKeyboard _keyboard; /// /// The that represents the location in the scene /// where the cursor should be drawn. /// IDrawable _sceneDrawLocation; } }