using System;
using System.Collections.Generic;
using System.Timers;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace TextEditorTest
{
public class Cursor
{
public int CharacterIndex = 0;
public string Symbol = "|";
public Vector2 Position = new Vector2(0, 0);
public int LineIndex = 0;
public bool Visible = true;
}
///
/// This is the main type for your game.
///
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private GapBuffer m_Text = new GapBuffer();
//private List m_CharacterPositions = new List();
private Dictionary m_CharacterPositions = new Dictionary();
private List m_HitBoxes = new List();
private bool m_UpdateCharPositions = true;
private const int NUM_CHARS_IN_LINE = 10;
private Vector2 m_TextEditPosition = new Vector2(50, 0), m_TextEditSize = new Vector2(250, 500);
private SpriteFont m_Font;
private Timer m_CursorVisibilityTimer = new Timer();
private Cursor m_Cursor = new Cursor();
private InputHelper m_Input = new InputHelper();
private int m_NumLinesInText = 1;
private bool m_HasFocus = true;
private bool m_MultiLine = true;
private bool m_CapitalLetters = false;
//For how long has a key been presseed?
private DateTime m_DownSince = DateTime.Now;
private float m_TimeUntilRepInMillis = 100f;
private int m_RepsPerSec = 15;
private DateTime m_LastRep = DateTime.Now;
private Keys? m_RepChar; //A character currently being pressed (repeated).
private Vector2 m_TextPosition = new Vector2(0, 0); //Coordinate for anchoring the text.
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
m_Cursor.Position = m_TextEditPosition;
m_CursorVisibilityTimer = new Timer(100);
m_CursorVisibilityTimer.Enabled = true;
m_CursorVisibilityTimer.Elapsed += CursorVisibilityTimer_Elapsed;
m_CursorVisibilityTimer.Start();
IsMouseVisible = true;
Window.TextInput += Window_TextInput;
m_TextPosition = m_TextEditPosition;
}
private void CursorVisibilityTimer_Elapsed(object sender, ElapsedEventArgs e)
{
if (m_HasFocus)
{
if (m_Cursor.Visible)
m_Cursor.Visible = false;
else
m_Cursor.Visible = true;
}
}
///
/// The text in this TextEditor instance, containing "\n".
///
private string TextWithLBreaks
{
get
{
string Text = "";
foreach (string Str in m_Text)
Text += Str;
return Text;
}
}
///
/// Returns a line of text.
///
/// The index of the line of text to get.
/// The line of text, as indicated by the line index.
private string GetLine(int LineIndex)
{
try
{
if (TextWithLBreaks.Contains("\n"))
return TextWithLBreaks.Split("\n".ToCharArray())[LineIndex];
}
catch(Exception)
{
return "";
}
return TextWithLBreaks;
}
///
/// Make sure cursor's index is valid and in range.
///
private void FixCursorIndex()
{
if (m_Cursor.CharacterIndex < 0)
m_Cursor.CharacterIndex = 0;
if (m_Cursor.CharacterIndex == m_Text.Count)
m_Cursor.CharacterIndex = m_Text.Count - 1;
}
///
/// Make sure cursor's position is valid and in range.
///
private void FixCursorPosition()
{
if (m_Cursor.Position.X < m_TextEditPosition.X)
m_Cursor.Position.X = m_TextEditPosition.X;
m_UpdateCharPositions = true;
UpdateCharacterPositions();
UpdateHitboxes();
//Find the curor's real character index.
int RealCharIndex = m_Cursor.CharacterIndex;
RealCharIndex = (RealCharIndex < m_CharacterPositions.Count) ? //Make sure it doesn't overflow.
RealCharIndex : m_CharacterPositions.Count - 1;
if (RealCharIndex < 0)
RealCharIndex = 0; //Make sure it doesn't underflow.
//Adjust the character's position based on the real character index.
if (m_Text.Count > 0)
{
Vector2 IndexPosition = m_CharacterPositions[(RealCharIndex > 0) ? RealCharIndex : 0];
if (m_Cursor.Position.X < IndexPosition.X)
m_Cursor.Position.X = IndexPosition.X;
if (m_Cursor.Position.Y != IndexPosition.Y)
m_Cursor.Position.Y = IndexPosition.Y;
}
}
///
/// The text in this TextEditor instance, without \n
/// (except for those explicitly added by pressing backspace).
///
public string Text
{
get
{
string Text = "";
foreach (string Str in m_Text)
Text += Str;
return Text.Replace("\n", "");
}
}
///
/// Returns the width of a character in this font.
///
/// The width of the character in floating point numbers.
private float CharacterWidth
{
get { return m_Font.MeasureString("a").X; }
}
///
/// Returns the width of a capitalized character in this font.
///
/// The width of the capitalized character in floating point numbers.
private float CapitalCharacterWidth
{
get { return m_Font.MeasureString("A").X; }
}
///
/// Returns the height of a character in this font.
///
/// The height of the character in floating point numbers.
private float CharacterHeight
{
get { return m_Font.MeasureString("a").Y; }
}
///
/// Returns the height of a capitalized character in this font.
///
/// The height of the capitalized character in floating point numbers.
private float CapitalCharacterHeight
{
get { return m_Font.MeasureString("A").Y; }
}
///
/// Returns the last line of text in the gap buffer.
///
///
private string CurrentLine
{
get
{
if (m_Text.Count > 1)
{
if (TextWithLBreaks.Contains("\n"))
{
string[] Lines = TextWithLBreaks.Split("\n".ToCharArray());
return Lines[Lines.Length - 1];
}
else
return TextWithLBreaks;
}
if (m_Text.Count > 0)
return m_Text[0];
else
return "";
}
}
///
/// The control received text input.
///
private void Window_TextInput(object sender, TextInputEventArgs e)
{
if (e.Character != (char)Keys.Back)
{
int Index = TextWithLBreaks.LastIndexOf("\n", m_Cursor.CharacterIndex);
if (Index == -1) //No occurence was found!!
{
if (Text.Length <= NUM_CHARS_IN_LINE)
{
AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString());
m_CapitalLetters = false;
m_UpdateCharPositions = true;
return;
}
else
{
if (m_MultiLine)
{
AddNewline();
return;
}
}
}
if ((m_Cursor.CharacterIndex - Index) <= NUM_CHARS_IN_LINE)
{
//If the cursor has moved away from the end of the text...
if (m_Cursor.CharacterIndex < (m_Text.Count - (1 + m_NumLinesInText)))
{
//... insert it at the cursor's position.
m_Text.Insert(m_Cursor.CharacterIndex, (m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString());
m_CapitalLetters = false;
m_UpdateCharPositions = true;
}
else
{
AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() :
e.Character.ToString()); //... just add the text as usual.
m_CapitalLetters = false;
m_UpdateCharPositions = true;
}
}
else
{
if(m_MultiLine)
AddNewline();
}
}
}
///
/// Adds a string to m_Text, and updates the cursor.
///
/// The string to add.
private void AddText(string Text)
{
m_Text.Add(Text);
m_Cursor.CharacterIndex++;
m_Cursor.Position.X += CharacterWidth;
}
///
/// Adds a newline to m_Text, and updates the cursor.
///
private void AddNewline()
{
m_Text.Add("\n");
m_Cursor.CharacterIndex++;
m_Cursor.Position.Y += CapitalCharacterHeight;
m_Cursor.Position.X = m_TextEditPosition.X;
m_Cursor.LineIndex++;
m_NumLinesInText++;
}
///
/// Removes text from m_Text.
///
private void RemoveText()
{
FixCursorIndex();
FixCursorPosition();
if (m_Cursor.Position.X > m_TextEditPosition.X)
{
m_Text.RemoveAt(m_Cursor.CharacterIndex);
m_Cursor.CharacterIndex--;
m_Cursor.Position.X -= CharacterWidth;
}
if (m_Cursor.Position.X <= m_TextEditPosition.X)
{
if (m_Cursor.LineIndex != 0)
{
m_Cursor.Position.X = m_TextEditPosition.X +
m_Font.MeasureString(GetLine(m_Cursor.LineIndex - 1)).X;
if (m_MultiLine)
{
m_Cursor.Position.Y -= CapitalCharacterHeight;
m_Cursor.LineIndex--;
m_NumLinesInText--;
}
}
}
}
///
/// Moves m_Cursor up.
///
private void MoveCursorUp()
{
if (m_Cursor.Position.Y > m_TextEditPosition.Y)
{
if (m_MultiLine)
{
m_Cursor.LineIndex--;
m_Cursor.CharacterIndex -= (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.Y -= CapitalCharacterHeight;
}
}
}
///
/// Moves m_Cursor down.
///
private void MoveCursorDown()
{
if (m_Cursor.Position.Y < (m_TextEditPosition.Y + m_TextEditSize.Y))
{
if (m_MultiLine)
{
m_Cursor.LineIndex++;
m_Cursor.CharacterIndex += (((NUM_CHARS_IN_LINE + 1) -
GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length);
m_Cursor.Position.Y += CapitalCharacterHeight;
}
}
}
///
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
///
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
///
/// LoadContent will be called once per game and is the place to load
/// all of your content.
///
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
m_Font = Content.Load("ProjectDollhouse_11px");
}
///
/// UnloadContent will be called once per game and is the place to unload
/// game-specific content.
///
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
///
/// Update the hitboxes for the characters in the textbox.
/// The hitboxes are used to detect collision(s) with the mouse cursor.
///
private void UpdateHitboxes()
{
if (m_UpdateCharPositions)
{
int Height = 0;
m_HitBoxes.Clear();
//Make sure it doesn't go out of bounds...
if (m_Text.Count >= 1)
{
for (int i = 0; i < m_CharacterPositions.Count; i++)
{
//Make sure it doesn't go out of bounds...
Height = (int)m_Font.MeasureString(m_Text[i < m_Text.Count ? i : m_Text.Count - 1]).Y;
//Create a hitbox for each character if the character isn't the last one.
if (i != m_CharacterPositions.Count - 1)
{
Rectangle Hitbox = new Rectangle((int)m_CharacterPositions[i].X, (int)m_CharacterPositions[i].Y,
(int)(m_CharacterPositions[i + 1].X - m_CharacterPositions[i].X), Height);
m_HitBoxes.Add(Hitbox);
}
}
}
m_UpdateCharPositions = false;
}
}
///
/// Updates the positions of the characters.
/// Called when a character is added or deleted from the textbox.
///
private void UpdateCharacterPositions()
{
Vector2 Position = m_TextEditPosition;
float XPosition = 0, YPosition = 0;
if (m_UpdateCharPositions)
{
m_CharacterPositions.Clear();
int CharIndex = 0;
foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray()))
{
XPosition = 0;
for (int i = 0; i < Str.Length; i++)
{
float CharWidth = m_Font.MeasureString(Str.Substring(i, 1)).X;
XPosition += CharWidth;
m_CharacterPositions.Add(CharIndex, new Vector2(XPosition + m_TextEditPosition.X, Position.Y + m_TextEditPosition.Y));
CharIndex++;
}
YPosition += CapitalCharacterHeight;
Position.Y = YPosition;
}
///This shouldn't be set here, because it is set in UpdateHitboxes();
//m_UpdateCharPositions = false;
}
}
///
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
///
/// Provides a snapshot of timing values.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
m_Input.Update();
UpdateCharacterPositions();
UpdateHitboxes();
foreach (Rectangle Hitbox in m_HitBoxes)
{
if (Hitbox.Contains(new Vector2(m_Input.MousePosition.X, m_Input.MousePosition.Y)) &&
m_Input.IsNewPress(MouseButtons.LeftButton))
{
m_Cursor.Position = new Vector2(Hitbox.X, Hitbox.Y);
int CharIndex = m_CharacterPositions.FirstOrDefault(x => x.Value == m_Cursor.Position).Key;
if (CharIndex != -1)
m_Cursor.CharacterIndex = CharIndex;
}
}
foreach (Keys Key in (Keys[])Enum.GetValues(typeof(Keys)))
{
if (m_Input.IsNewPress(Key))
{
m_DownSince = DateTime.Now;
m_RepChar = Key;
}
else if (m_Input.IsOldPress(Key))
{
if (m_RepChar == Key)
m_RepChar = null;
}
if (m_RepChar != null && m_RepChar == Key && m_Input.CurrentKeyboardState.IsKeyDown(Key))
{
DateTime Now = DateTime.Now;
TimeSpan DownFor = Now.Subtract(m_DownSince);
if (DownFor.CompareTo(TimeSpan.FromMilliseconds(m_TimeUntilRepInMillis)) > 0)
{
// Should repeat since the wait time is over now.
TimeSpan repeatSince = Now.Subtract(m_LastRep);
if (repeatSince.CompareTo(TimeSpan.FromMilliseconds(1000f / m_RepsPerSec)) > 0)
// Time for another key-stroke.
m_LastRep = Now;
}
}
}
Keys[] PressedKeys = m_Input.CurrentKeyboardState.GetPressedKeys();
//Are these keys being held down since the last update?
if (m_RepChar == Keys.Back && m_LastRep == DateTime.Now)
RemoveText();
if (m_RepChar == Keys.Up && m_LastRep == DateTime.Now)
MoveCursorUp();
if (m_RepChar == Keys.Down && m_LastRep == DateTime.Now)
MoveCursorDown();
foreach (Keys K in PressedKeys)
{
if (m_Input.IsNewPress(K))
{
switch(K)
{
case Keys.Up:
if (m_RepChar != Keys.Up || m_LastRep != DateTime.Now)
MoveCursorUp();
break;
case Keys.Down:
if (m_RepChar != Keys.Down || m_LastRep != DateTime.Now)
MoveCursorDown();
break;
case Keys.Left:
if (m_Cursor.Position.X > m_TextEditPosition.X)
{
m_Cursor.CharacterIndex--;
m_Cursor.Position.X -= CharacterWidth;
}
break;
case Keys.Right:
if (m_Cursor.Position.X < (m_TextEditPosition.X + m_TextEditSize.X))
{
m_Cursor.CharacterIndex--;
m_Cursor.Position.X += CharacterWidth;
}
break;
case Keys.Back:
if (m_RepChar != Keys.Back || m_LastRep != DateTime.Now)
RemoveText();
break;
case Keys.LeftShift:
m_CapitalLetters = true;
break;
case Keys.RightShift:
m_CapitalLetters = true;
break;
}
}
}
base.Update(gameTime);
}
///
/// This is called when the game should draw itself.
///
/// Provides a snapshot of timing values.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle((int)m_TextEditPosition.X,
(int)m_TextEditPosition.Y, (int)m_TextEditSize.X, (int)m_TextEditSize.Y);
Vector2 Position = m_TextPosition;
spriteBatch.Begin(SpriteSortMode.BackToFront);
if(m_Cursor.Visible)
spriteBatch.DrawString(m_Font, m_Cursor.Symbol, m_Cursor.Position, Color.White);
// TODO: Add your drawing code here
foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray()))
{
spriteBatch.DrawString(m_Font, Str, Position, Color.White);
Position.Y += CapitalCharacterHeight;
}
spriteBatch.End();
base.Draw(gameTime);
}
}
}