// Unity C# reference source // Copyright (c) Unity Technologies. For terms of use, see // https://unity3d.com/legal/licenses/Unity_Reference_Only_License using System; using UnityEngine.Bindings; using UnityEngine.Scripting; using System.Collections.Generic; namespace UnityEngine { public class TextEditor { public TouchScreenKeyboard keyboardOnScreen = null; public int controlID = 0; public GUIStyle style = GUIStyle.none; public bool multiline = false; public bool hasHorizontalCursorPos = false; public bool isPasswordField = false; internal bool m_HasFocus; public Vector2 scrollOffset = Vector2.zero; // The text field can have a scroll offset in order to display its contents private GUIContent m_Content = new GUIContent(); private Rect m_Position; private int m_CursorIndex = 0; private int m_SelectIndex = 0; private bool m_RevealCursor = false; [Obsolete("Please use 'text' instead of 'content'", false)] public GUIContent content { get { return m_Content; } set { m_Content = value; } } public string text { get { return m_Content.text; } set { m_Content.text = value ?? string.Empty; EnsureValidCodePointIndex(ref m_CursorIndex); EnsureValidCodePointIndex(ref m_SelectIndex); } } public Rect position { get { return m_Position; } set { if (m_Position == value) return; // Reset the scrollOffset to force its recomputation. scrollOffset = Vector2.zero; m_Position = value; UpdateScrollOffset(); } } internal virtual Rect localPosition { get { return position; } } public int cursorIndex { get { return m_CursorIndex; } set { int oldCursorIndex = m_CursorIndex; m_CursorIndex = value; EnsureValidCodePointIndex(ref m_CursorIndex); if (m_CursorIndex != oldCursorIndex) { m_RevealCursor = true; OnCursorIndexChange(); } } } public int selectIndex { get { return m_SelectIndex; } set { int oldSelectIndex = m_SelectIndex; m_SelectIndex = value; EnsureValidCodePointIndex(ref m_SelectIndex); if (m_SelectIndex != oldSelectIndex) OnSelectIndexChange(); } } // are we up/downing? public Vector2 graphicalCursorPos; public Vector2 graphicalSelectCursorPos; // Clear the cursor position for vertical movement... void ClearCursorPos() {hasHorizontalCursorPos = false; m_iAltCursorPos = -1; } // selection bool m_MouseDragSelectsWholeWords = false; int m_DblClickInitPos = 0; DblClickSnapping m_DblClickSnap = DblClickSnapping.WORDS; public DblClickSnapping doubleClickSnapping { get { return m_DblClickSnap; } set { m_DblClickSnap = value; } } bool m_bJustSelected = false; int m_iAltCursorPos = -1; public int altCursorPosition { get { return m_iAltCursorPos; } set { m_iAltCursorPos = value; } } public enum DblClickSnapping : byte { WORDS, PARAGRAPHS } [RequiredByNativeCode] public TextEditor() { } public void OnFocus() { if (multiline) cursorIndex = selectIndex = 0; else SelectAll(); m_HasFocus = true; } public void OnLostFocus() { m_HasFocus = false; scrollOffset = Vector2.zero; } void GrabGraphicalCursorPos() { if (!hasHorizontalCursorPos) { graphicalCursorPos = style.GetCursorPixelPosition(localPosition, m_Content, cursorIndex); graphicalSelectCursorPos = style.GetCursorPixelPosition(localPosition, m_Content, selectIndex); hasHorizontalCursorPos = false; } } // Handle a key event. // Looks up the platform-dependent key-action table & performs the event // return true if the event was recognized. public bool HandleKeyEvent(Event e) { return HandleKeyEvent(e, false); } [VisibleToOtherModules] internal bool HandleKeyEvent(Event e, bool textIsReadOnly) { InitKeyActions(); EventModifiers m = e.modifiers; e.modifiers &= ~EventModifiers.CapsLock; if (s_Keyactions.ContainsKey(e)) { TextEditOp op = (TextEditOp)s_Keyactions[e]; PerformOperation(op, textIsReadOnly); e.modifiers = m; return true; } e.modifiers = m; return false; } // Deletes previous text on the line public bool DeleteLineBack() { if (hasSelection) { DeleteSelection(); return true; } int p = cursorIndex; int i = p; while (i-- != 0) if (text[i] == '\n') { p = i + 1; break; } if (i == -1) p = 0; if (cursorIndex != p) { m_Content.text = text.Remove(p, cursorIndex - p); selectIndex = cursorIndex = p; return true; } return false; } // Deletes the previous word public bool DeleteWordBack() { if (hasSelection) { DeleteSelection(); return true; } int prevWordEnd = FindEndOfPreviousWord(cursorIndex); if (cursorIndex != prevWordEnd) { m_Content.text = text.Remove(prevWordEnd, cursorIndex - prevWordEnd); selectIndex = cursorIndex = prevWordEnd; return true; } return false; } // Deletes the following word public bool DeleteWordForward() { if (hasSelection) { DeleteSelection(); return true; } int nextWordStart = FindStartOfNextWord(cursorIndex); if (cursorIndex < text.Length) { m_Content.text = text.Remove(cursorIndex, nextWordStart - cursorIndex); return true; } return false; } // perform a right-delete public bool Delete() { if (hasSelection) { DeleteSelection(); return true; } else if (cursorIndex < text.Length) { m_Content.text = text.Remove(cursorIndex, NextCodePointIndex(cursorIndex) - cursorIndex); return true; } return false; } public bool CanPaste() { return GUIUtility.systemCopyBuffer.Length != 0; } // Perform a left-delete public bool Backspace() { if (hasSelection) { DeleteSelection(); return true; } else if (cursorIndex > 0) { var startIndex = PreviousCodePointIndex(cursorIndex); m_Content.text = text.Remove(startIndex, cursorIndex - startIndex); selectIndex = cursorIndex = startIndex; ClearCursorPos(); return true; } return false; } /// Select all the text public void SelectAll() { cursorIndex = 0; selectIndex = text.Length; ClearCursorPos(); } /// Select none of the text public void SelectNone() { selectIndex = cursorIndex; ClearCursorPos(); } /// Does this text field has a selection public bool hasSelection { get { return cursorIndex != selectIndex; } } /// Returns the selected text public string SelectedText { get { if (cursorIndex == selectIndex) return ""; if (cursorIndex < selectIndex) return text.Substring(cursorIndex, selectIndex - cursorIndex); else return text.Substring(selectIndex, cursorIndex - selectIndex); } } /// Delete the current selection. If there is no selection, this function does not do anything... public bool DeleteSelection() { if (cursorIndex == selectIndex) return false; if (cursorIndex < selectIndex) { m_Content.text = text.Substring(0, cursorIndex) + text.Substring(selectIndex, text.Length - selectIndex); selectIndex = cursorIndex; } else { m_Content.text = text.Substring(0, selectIndex) + text.Substring(cursorIndex, text.Length - cursorIndex); cursorIndex = selectIndex; } ClearCursorPos(); return true; } /// Replace the selection with /replace/. If there is no selection, /replace/ is inserted at the current cursor point. public void ReplaceSelection(string replace) { DeleteSelection(); m_Content.text = text.Insert(cursorIndex, replace); selectIndex = cursorIndex += replace.Length; ClearCursorPos(); } /// Replacted the selection with /c/ public void Insert(char c) { ReplaceSelection(c.ToString()); } /// Move selection to alt cursor /position/ public void MoveSelectionToAltCursor() { if (m_iAltCursorPos == -1) return; int p = m_iAltCursorPos; string tmp = SelectedText; m_Content.text = text.Insert(p, tmp); if (p < cursorIndex) { cursorIndex += tmp.Length; selectIndex += tmp.Length; } DeleteSelection(); selectIndex = cursorIndex = p; ClearCursorPos(); } /// Move the cursor one character to the right and deselect. public void MoveRight() { ClearCursorPos(); if (selectIndex == cursorIndex) { cursorIndex = NextCodePointIndex(cursorIndex); DetectFocusChange(); // TODO: Is this necessary? selectIndex = cursorIndex; } else { if (selectIndex > cursorIndex) cursorIndex = selectIndex; else selectIndex = cursorIndex; } } /// Move the cursor one character to the left and deselect. public void MoveLeft() { if (selectIndex == cursorIndex) { cursorIndex = PreviousCodePointIndex(cursorIndex); selectIndex = cursorIndex; } else { if (selectIndex > cursorIndex) selectIndex = cursorIndex; else cursorIndex = selectIndex; } ClearCursorPos(); } /// Move the cursor up and deselects. public void MoveUp() { if (selectIndex < cursorIndex) selectIndex = cursorIndex; else cursorIndex = selectIndex; GrabGraphicalCursorPos(); graphicalCursorPos.y -= 1; cursorIndex = selectIndex = style.GetCursorStringIndex(localPosition, m_Content, graphicalCursorPos); if (cursorIndex <= 0) ClearCursorPos(); } /// Move the cursor down and deselects. public void MoveDown() { if (selectIndex > cursorIndex) selectIndex = cursorIndex; else cursorIndex = selectIndex; GrabGraphicalCursorPos(); graphicalCursorPos.y += style.lineHeight + 5; cursorIndex = selectIndex = style.GetCursorStringIndex(localPosition, m_Content, graphicalCursorPos); if (cursorIndex == text.Length) ClearCursorPos(); } /// Moves the cursor to the start of the current line. public void MoveLineStart() { // we start from the left-most selected character int p = selectIndex < cursorIndex ? selectIndex : cursorIndex; // then we scan back to find the first newline int i = p; while (i-- != 0) if (text[i] == '\n') { selectIndex = cursorIndex = i + 1; return; } selectIndex = cursorIndex = 0; } /// Moves the selection to the end of the current line public void MoveLineEnd() { // we start from the right-most selected character int p = selectIndex > cursorIndex ? selectIndex : cursorIndex; // then we scan forward to find the first newline int i = p; int strlen = text.Length; while (i < strlen) { if (text[i] == '\n') { selectIndex = cursorIndex = i; return; } i++; } selectIndex = cursorIndex = strlen; } /// Move to the start of the current graphical line. This takes word-wrapping into consideration. public void MoveGraphicalLineStart() { cursorIndex = selectIndex = GetGraphicalLineStart(cursorIndex < selectIndex ? cursorIndex : selectIndex); } /// Move to the end of the current graphical line. This takes word-wrapping into consideration. public void MoveGraphicalLineEnd() { cursorIndex = selectIndex = GetGraphicalLineEnd(cursorIndex > selectIndex ? cursorIndex : selectIndex); } /// Moves the cursor to the beginning of the text public void MoveTextStart() { selectIndex = cursorIndex = 0; } /// Moves the cursor to the end of the text public void MoveTextEnd() { selectIndex = cursorIndex = text.Length; } private int IndexOfEndOfLine(int startIndex) { int index = text.IndexOf('\n', startIndex); return (index != -1 ? index : text.Length); } /// Move to the next paragraph public void MoveParagraphForward() { cursorIndex = cursorIndex > selectIndex ? cursorIndex : selectIndex; if (cursorIndex < text.Length) { selectIndex = cursorIndex = IndexOfEndOfLine(cursorIndex + 1); } } /// Move to the previous paragraph public void MoveParagraphBackward() { cursorIndex = cursorIndex < selectIndex ? cursorIndex : selectIndex; if (cursorIndex > 1) { selectIndex = cursorIndex = text.LastIndexOf('\n', cursorIndex - 2) + 1; } else selectIndex = cursorIndex = 0; } // // Move the cursor to a graphical position. Used for moving the cursor on MouseDown events. public void MoveCursorToPosition(Vector2 cursorPosition) { MoveCursorToPosition_Internal(cursorPosition, Event.current.shift); } // Move the cursor to a graphical position. Used for moving the cursor on MouseDown events. protected internal void MoveCursorToPosition_Internal(Vector2 cursorPosition, bool shift) { selectIndex = style.GetCursorStringIndex(localPosition, m_Content, cursorPosition + scrollOffset); if (!shift) { cursorIndex = selectIndex; } DetectFocusChange(); // TODO: Is this necessary? } public void MoveAltCursorToPosition(Vector2 cursorPosition) { int index = style.GetCursorStringIndex(localPosition, m_Content, cursorPosition + scrollOffset); m_iAltCursorPos = Mathf.Min(text.Length, index); DetectFocusChange(); // TODO: Is this necessary? } public bool IsOverSelection(Vector2 cursorPosition) { int p = style.GetCursorStringIndex(localPosition, m_Content, cursorPosition + scrollOffset); return ((p < Mathf.Max(cursorIndex, selectIndex)) && (p > Mathf.Min(cursorIndex, selectIndex))); } // Do a drag selection. Used to expand the selection in MouseDrag events. public void SelectToPosition(Vector2 cursorPosition) { if (!m_MouseDragSelectsWholeWords) cursorIndex = style.GetCursorStringIndex(localPosition, m_Content, cursorPosition + scrollOffset); else // snap to words/paragraphs { int p = style.GetCursorStringIndex(localPosition, m_Content, cursorPosition + scrollOffset); EnsureValidCodePointIndex(ref p); EnsureValidCodePointIndex(ref m_DblClickInitPos); if (m_DblClickSnap == DblClickSnapping.WORDS) { if (p < m_DblClickInitPos) { cursorIndex = FindEndOfClassification(p, Direction.Backward); selectIndex = FindEndOfClassification(m_DblClickInitPos, Direction.Forward); } else { cursorIndex = FindEndOfClassification(p, Direction.Forward); selectIndex = FindEndOfClassification(m_DblClickInitPos, Direction.Backward); } } // paragraph else { if (p < m_DblClickInitPos) { if (p > 0) cursorIndex = text.LastIndexOf('\n', Mathf.Max(0, p - 2)) + 1; else cursorIndex = 0; selectIndex = text.LastIndexOf('\n', Mathf.Min(text.Length - 1, m_DblClickInitPos)); } else { if (p < text.Length) { cursorIndex = IndexOfEndOfLine(p); } else cursorIndex = text.Length; selectIndex = text.LastIndexOf('\n', Mathf.Max(0, m_DblClickInitPos - 2)) + 1; } } } } /// Expand the selection to the left public void SelectLeft() { if (m_bJustSelected) if (cursorIndex > selectIndex) { // swap int tmp = cursorIndex; cursorIndex = selectIndex; selectIndex = tmp; } m_bJustSelected = false; cursorIndex = PreviousCodePointIndex(cursorIndex); } public void SelectRight() { if (m_bJustSelected) if (cursorIndex < selectIndex) { // swap int tmp = cursorIndex; cursorIndex = selectIndex; selectIndex = tmp; } m_bJustSelected = false; cursorIndex = NextCodePointIndex(cursorIndex); } public void SelectUp() { GrabGraphicalCursorPos(); graphicalCursorPos.y -= 1; cursorIndex = style.GetCursorStringIndex(localPosition, m_Content, graphicalCursorPos); } public void SelectDown() { GrabGraphicalCursorPos(); graphicalCursorPos.y += style.lineHeight + 5; cursorIndex = style.GetCursorStringIndex(localPosition, m_Content, graphicalCursorPos); } /// Select to the end of the text public void SelectTextEnd() { // This is not quite like the mac - there, when you select to end of text, the position of the cursor becomes somewhat i'll defined // Hard to explain. In textedit, try: CMD-SHIFT-down, SHIFT-LEFT for case 1. then do CMD-SHIFT-down, SHIFT-RIGHT, SHIFT-LEFT for case 2. // Anyways, it's wrong so we won't do that cursorIndex = text.Length; } /// Select to the start of the text public void SelectTextStart() { // Same thing as SelectTextEnd... cursorIndex = 0; } /// sets whether the text selection is done by dbl click or not public void MouseDragSelectsWholeWords(bool on) { m_MouseDragSelectsWholeWords = on; m_DblClickInitPos = cursorIndex; } public void DblClickSnap(DblClickSnapping snapping) { m_DblClickSnap = snapping; } int GetGraphicalLineStart(int p) { Vector2 point = style.GetCursorPixelPosition(localPosition, m_Content, p); point.y += 1.0f / GUIUtility.pixelsPerPoint; // we make sure no floating point errors can make us land on another line point.x = 0; return style.GetCursorStringIndex(localPosition, m_Content, point); } int GetGraphicalLineEnd(int p) { Vector2 point = style.GetCursorPixelPosition(localPosition, m_Content, p); point.y += 1.0f / GUIUtility.pixelsPerPoint; // we make sure no floating point errors can make us land on another line point.x += 5000; return style.GetCursorStringIndex(localPosition, m_Content, point); } int FindNextSeperator(int startPos) { int textLen = text.Length; while (startPos < textLen && ClassifyChar(startPos) != CharacterType.LetterLike) startPos = NextCodePointIndex(startPos); while (startPos < textLen && ClassifyChar(startPos) == CharacterType.LetterLike) startPos = NextCodePointIndex(startPos); return startPos; } int FindPrevSeperator(int startPos) { startPos = PreviousCodePointIndex(startPos); while (startPos > 0 && ClassifyChar(startPos) != CharacterType.LetterLike) startPos = PreviousCodePointIndex(startPos); if (startPos == 0) return 0; while (startPos > 0 && ClassifyChar(startPos) == CharacterType.LetterLike) startPos = PreviousCodePointIndex(startPos); if (ClassifyChar(startPos) == CharacterType.LetterLike) return startPos; return NextCodePointIndex(startPos); } /// Move to the end of the word. /// If the cursor is over some space characters, these are skipped /// Then, the cursor moves to the end of the following word. /// This corresponds to Alt-RightArrow on a Mac public void MoveWordRight() { cursorIndex = cursorIndex > selectIndex ? cursorIndex : selectIndex; cursorIndex = selectIndex = FindNextSeperator(cursorIndex); ClearCursorPos(); } public void MoveToStartOfNextWord() { ClearCursorPos(); if (cursorIndex != selectIndex) { MoveRight(); return; } cursorIndex = selectIndex = FindStartOfNextWord(cursorIndex); } public void MoveToEndOfPreviousWord() { ClearCursorPos(); if (cursorIndex != selectIndex) { MoveLeft(); return; } cursorIndex = selectIndex = FindEndOfPreviousWord(cursorIndex); } public void SelectToStartOfNextWord() { ClearCursorPos(); cursorIndex = FindStartOfNextWord(cursorIndex); } public void SelectToEndOfPreviousWord() { ClearCursorPos(); cursorIndex = FindEndOfPreviousWord(cursorIndex); } enum CharacterType { LetterLike, Symbol, Symbol2, WhiteSpace } CharacterType ClassifyChar(int index) { if (char.IsWhiteSpace(text, index)) return CharacterType.WhiteSpace; if (char.IsLetterOrDigit(text, index) || text[index] == '\'') return CharacterType.LetterLike; return CharacterType.Symbol; } /// Move to start of next word. /// This corresponds to Ctrl-RightArrow on Windows /// If the cursor is over a whitespace, it's moved forwards ''till the first non-whitespace character /// If the cursor is over an alphanumeric character, it''s moved forward 'till it encounters space or a punctuation mark. /// If the stopping character is a space, this is skipped as well. /// If the cursor is over an punctuation mark, it's moved forward ''till it a letter or a space of a punctuation mark. If the stopping character is a space, this is skipped as well public int FindStartOfNextWord(int p) { int textLen = text.Length; if (p == textLen) return p; // Find out which char type we're at... CharacterType t = ClassifyChar(p); if (t != CharacterType.WhiteSpace) { p = NextCodePointIndex(p); while (p < textLen && ClassifyChar(p) == t) p = NextCodePointIndex(p); } else { if (text[p] == '\t' || text[p] == '\n') return NextCodePointIndex(p); } if (p == textLen) return p; // Skip spaces if (text[p] == ' ') // If we're at a space, skip over any number of spaces { while (p < textLen && ClassifyChar(p) == CharacterType.WhiteSpace) p = NextCodePointIndex(p); } else if (text[p] == '\t' || text[p] == '\n') // If we're at a tab or a newline, just step one char ahead { return p; } return p; } int FindEndOfPreviousWord(int p) { if (p == 0) return p; p = PreviousCodePointIndex(p); // Skip spaces while (p > 0 && text[p] == ' ') p = PreviousCodePointIndex(p); CharacterType t = ClassifyChar(p); if (t != CharacterType.WhiteSpace) { while (p > 0 && ClassifyChar(PreviousCodePointIndex(p)) == t) p = PreviousCodePointIndex(p); } return p; } public void MoveWordLeft() { cursorIndex = cursorIndex < selectIndex ? cursorIndex : selectIndex; cursorIndex = FindPrevSeperator(cursorIndex); selectIndex = cursorIndex; } public void SelectWordRight() { ClearCursorPos(); int cachedPos = selectIndex; if (cursorIndex < selectIndex) { selectIndex = cursorIndex; MoveWordRight(); selectIndex = cachedPos; cursorIndex = cursorIndex < selectIndex ? cursorIndex : selectIndex; return; } selectIndex = cursorIndex; MoveWordRight(); selectIndex = cachedPos; } public void SelectWordLeft() { ClearCursorPos(); int cachedPos = selectIndex; if (cursorIndex > selectIndex) { selectIndex = cursorIndex; MoveWordLeft(); selectIndex = cachedPos; cursorIndex = cursorIndex > selectIndex ? cursorIndex : selectIndex; return; } selectIndex = cursorIndex; MoveWordLeft(); selectIndex = cachedPos; } /// Expand the selection to the start of the line /// Used on a mac for CMD-SHIFT-LEFT public void ExpandSelectGraphicalLineStart() { ClearCursorPos(); if (cursorIndex < selectIndex) cursorIndex = GetGraphicalLineStart(cursorIndex); else { int temp = cursorIndex; cursorIndex = GetGraphicalLineStart(selectIndex); selectIndex = temp; } } /// Expand the selection to the end of the line /// Used on a mac for CMD-SHIFT-RIGHT public void ExpandSelectGraphicalLineEnd() { ClearCursorPos(); if (cursorIndex > selectIndex) cursorIndex = GetGraphicalLineEnd(cursorIndex); else { int temp = cursorIndex; cursorIndex = GetGraphicalLineEnd(selectIndex); selectIndex = temp; } } /// Move the selection point to the start of the line /// Used on a Windows for SHIFT-Home public void SelectGraphicalLineStart() { ClearCursorPos(); cursorIndex = GetGraphicalLineStart(cursorIndex); } /// Expand the selection to the end of the line /// Used on a mac for SHIFT-End public void SelectGraphicalLineEnd() { ClearCursorPos(); cursorIndex = GetGraphicalLineEnd(cursorIndex); } public void SelectParagraphForward() { ClearCursorPos(); bool wasBehind = cursorIndex < selectIndex; if (cursorIndex < text.Length) { cursorIndex = IndexOfEndOfLine(cursorIndex + 1); if (wasBehind && cursorIndex > selectIndex) cursorIndex = selectIndex; } } public void SelectParagraphBackward() { ClearCursorPos(); bool wasInFront = cursorIndex > selectIndex; if (cursorIndex > 1) { cursorIndex = text.LastIndexOf('\n', cursorIndex - 2) + 1; if (wasInFront && cursorIndex < selectIndex) cursorIndex = selectIndex; } else selectIndex = cursorIndex = 0; } /// Select the word under the cursor public void SelectCurrentWord() { var index = cursorIndex; if (cursorIndex < selectIndex) { cursorIndex = FindEndOfClassification(index, Direction.Backward); selectIndex = FindEndOfClassification(index, Direction.Forward); } else { cursorIndex = FindEndOfClassification(index, Direction.Forward); selectIndex = FindEndOfClassification(index, Direction.Backward); } ClearCursorPos(); m_bJustSelected = true; } enum Direction { Forward, Backward, } int FindEndOfClassification(int p, Direction dir) { if (text.Length == 0) return 0; if (p == text.Length) p = PreviousCodePointIndex(p); var t = ClassifyChar(p); do { switch (dir) { case Direction.Backward: p = PreviousCodePointIndex(p); if (p == 0) return ClassifyChar(0) == t ? 0 : NextCodePointIndex(0); break; case Direction.Forward: p = NextCodePointIndex(p); if (p == text.Length) return text.Length; break; } } while (ClassifyChar(p) == t); if (dir == Direction.Forward) return p; return NextCodePointIndex(p); } // Select the entire paragraph the cursor is on (separated by \n) public void SelectCurrentParagraph() { ClearCursorPos(); int textLen = text.Length; if (cursorIndex < textLen) { cursorIndex = IndexOfEndOfLine(cursorIndex) + 1; } if (selectIndex != 0) selectIndex = text.LastIndexOf('\n', selectIndex - 1) + 1; } public void UpdateScrollOffsetIfNeeded(Event evt) { if (evt.type != EventType.Repaint && evt.type != EventType.Layout) { UpdateScrollOffset(); } } [VisibleToOtherModules] internal void UpdateScrollOffset() { int cursorPos = cursorIndex; graphicalCursorPos = style.GetCursorPixelPosition(new Rect(0, 0, position.width, position.height), m_Content, cursorPos); // The rectangle inside which the text is displayed. Rect viewRect = style.padding.Remove(position); // Position of the cursor in the viewRect coordinate system. var localGraphicalCursorPos = graphicalCursorPos; localGraphicalCursorPos.x -= style.padding.left; localGraphicalCursorPos.y -= style.padding.top; // The size of the text, without any padding. Vector2 contentSize = new Vector2(style.CalcSize(m_Content).x, style.CalcHeight(m_Content, position.width)); contentSize.x -= (style.padding.left + style.padding.right); contentSize.y -= (style.padding.top + style.padding.bottom); // If there is plenty of room, simply show entire string if (contentSize.x < viewRect.width) { scrollOffset.x = 0; } else if (m_RevealCursor) { //go right if (localGraphicalCursorPos.x + 1 > scrollOffset.x + viewRect.width) // do we want html or apple behavior? this is html behavior scrollOffset.x = localGraphicalCursorPos.x - viewRect.width + 1; //go left if (localGraphicalCursorPos.x < scrollOffset.x) scrollOffset.x = localGraphicalCursorPos.x; } // ... and height/y as well // If there is plenty of room, simply show entire string if (contentSize.y < viewRect.height) { scrollOffset.y = 0; } else if (m_RevealCursor) { //go down if (localGraphicalCursorPos.y + style.lineHeight > scrollOffset.y + viewRect.height) scrollOffset.y = localGraphicalCursorPos.y - viewRect.height + style.lineHeight; //go up if (localGraphicalCursorPos.y < scrollOffset.y) scrollOffset.y = localGraphicalCursorPos.y; } // This case takes many words to explain: // 1. Text field has more text than it can fit vertically, and the cursor is at the very bottom (text field is scrolled down) // 2. user e.g. deletes some lines of text at the bottom (backspace or select+delete) // 3. now suddenly we have space at the bottom of text field, that is now not filled with any content // 4. scroll text field up to fill in that space (this is what other text editors do) if (scrollOffset.y > 0 && contentSize.y - scrollOffset.y < viewRect.height) scrollOffset.y = contentSize.y - viewRect.height; scrollOffset.y = scrollOffset.y < 0 ? 0 : scrollOffset.y; m_RevealCursor = false; } // TODO: get the height from the font public void DrawCursor(string newText) { string realText = text; int cursorPos = cursorIndex; if (GUIUtility.compositionString.Length > 0) { m_Content.text = newText.Substring(0, cursorIndex) + GUIUtility.compositionString + newText.Substring(selectIndex); cursorPos += GUIUtility.compositionString.Length; } else m_Content.text = newText; graphicalCursorPos = style.GetCursorPixelPosition(new Rect(0, 0, position.width, position.height), m_Content, cursorPos); //Debug.Log("Cursor pos: " + graphicalCursorPos); Vector2 originalContentOffset = style.contentOffset; style.contentOffset -= scrollOffset; style.Internal_clipOffset = scrollOffset; // Debug.Log ("ScrollOffset : " + scrollOffset); GUIUtility.compositionCursorPos = GUIClip.UnclipToWindow(graphicalCursorPos + new Vector2(position.x, position.y + style.lineHeight) - scrollOffset); if (GUIUtility.compositionString.Length > 0) style.DrawWithTextSelection(position, m_Content, controlID, cursorIndex, cursorIndex + GUIUtility.compositionString.Length, true); else style.DrawWithTextSelection(position, m_Content, controlID, cursorIndex, selectIndex); if (m_iAltCursorPos != -1) style.DrawCursor(position, m_Content, controlID, m_iAltCursorPos); // reset style.contentOffset = originalContentOffset; style.Internal_clipOffset = Vector2.zero; m_Content.text = realText; } bool PerformOperation(TextEditOp operation, bool textIsReadOnly) { m_RevealCursor = true; switch (operation) { // NOTE the TODOs below: case TextEditOp.MoveLeft: MoveLeft(); break; case TextEditOp.MoveRight: MoveRight(); break; case TextEditOp.MoveUp: MoveUp(); break; case TextEditOp.MoveDown: MoveDown(); break; case TextEditOp.MoveLineStart: MoveLineStart(); break; case TextEditOp.MoveLineEnd: MoveLineEnd(); break; case TextEditOp.MoveWordRight: MoveWordRight(); break; case TextEditOp.MoveToStartOfNextWord: MoveToStartOfNextWord(); break; case TextEditOp.MoveToEndOfPreviousWord: MoveToEndOfPreviousWord(); break; case TextEditOp.MoveWordLeft: MoveWordLeft(); break; case TextEditOp.MoveTextStart: MoveTextStart(); break; case TextEditOp.MoveTextEnd: MoveTextEnd(); break; case TextEditOp.MoveParagraphForward: MoveParagraphForward(); break; case TextEditOp.MoveParagraphBackward: MoveParagraphBackward(); break; // case TextEditOp.MovePageUp: return MovePageUp (); break; // case TextEditOp.MovePageDown: return MovePageDown (); break; case TextEditOp.MoveGraphicalLineStart: MoveGraphicalLineStart(); break; case TextEditOp.MoveGraphicalLineEnd: MoveGraphicalLineEnd(); break; case TextEditOp.SelectLeft: SelectLeft(); break; case TextEditOp.SelectRight: SelectRight(); break; case TextEditOp.SelectUp: SelectUp(); break; case TextEditOp.SelectDown: SelectDown(); break; case TextEditOp.SelectWordRight: SelectWordRight(); break; case TextEditOp.SelectWordLeft: SelectWordLeft(); break; case TextEditOp.SelectToEndOfPreviousWord: SelectToEndOfPreviousWord(); break; case TextEditOp.SelectToStartOfNextWord: SelectToStartOfNextWord(); break; case TextEditOp.SelectTextStart: SelectTextStart(); break; case TextEditOp.SelectTextEnd: SelectTextEnd(); break; case TextEditOp.ExpandSelectGraphicalLineStart: ExpandSelectGraphicalLineStart(); break; case TextEditOp.ExpandSelectGraphicalLineEnd: ExpandSelectGraphicalLineEnd(); break; case TextEditOp.SelectParagraphForward: SelectParagraphForward(); break; case TextEditOp.SelectParagraphBackward: SelectParagraphBackward(); break; case TextEditOp.SelectGraphicalLineStart: SelectGraphicalLineStart(); break; case TextEditOp.SelectGraphicalLineEnd: SelectGraphicalLineEnd(); break; // case TextEditOp.SelectPageUp: return SelectPageUp (); break; // case TextEditOp.SelectPageDown: return SelectPageDown (); break; case TextEditOp.Delete: if (textIsReadOnly) return false; else return Delete(); case TextEditOp.Backspace: if (textIsReadOnly) return false; else return Backspace(); case TextEditOp.Cut: if (textIsReadOnly) return false; else return Cut(); case TextEditOp.Copy: Copy(); break; case TextEditOp.Paste: if (textIsReadOnly) return false; else return Paste(); case TextEditOp.SelectAll: SelectAll(); break; case TextEditOp.SelectNone: SelectNone(); break; // case TextEditOp.ScrollStart: return ScrollStart (); break; // case TextEditOp.ScrollEnd: return ScrollEnd (); break; // case TextEditOp.ScrollPageUp: return ScrollPageUp (); break; // case TextEditOp.ScrollPageDown: return ScrollPageDown (); break; case TextEditOp.DeleteWordBack: if (textIsReadOnly) return false; else return DeleteWordBack(); case TextEditOp.DeleteLineBack: if (textIsReadOnly) return false; else return DeleteLineBack(); case TextEditOp.DeleteWordForward: if (textIsReadOnly) return false; else return DeleteWordForward(); default: Debug.Log("Unimplemented: " + operation); break; } return false; } enum TextEditOp { MoveLeft, MoveRight, MoveUp, MoveDown, MoveLineStart, MoveLineEnd, MoveTextStart, MoveTextEnd, MovePageUp, MovePageDown, MoveGraphicalLineStart, MoveGraphicalLineEnd, MoveWordLeft, MoveWordRight, MoveParagraphForward, MoveParagraphBackward, MoveToStartOfNextWord, MoveToEndOfPreviousWord, SelectLeft, SelectRight, SelectUp, SelectDown, SelectTextStart, SelectTextEnd, SelectPageUp, SelectPageDown, ExpandSelectGraphicalLineStart, ExpandSelectGraphicalLineEnd, SelectGraphicalLineStart, SelectGraphicalLineEnd, SelectWordLeft, SelectWordRight, SelectToEndOfPreviousWord, SelectToStartOfNextWord, SelectParagraphBackward, SelectParagraphForward, Delete, Backspace, DeleteWordBack, DeleteWordForward, DeleteLineBack, Cut, Copy, Paste, SelectAll, SelectNone, ScrollStart, ScrollEnd, ScrollPageUp, ScrollPageDown } string oldText; int oldPos, oldSelectPos; public void SaveBackup() { oldText = text; oldPos = cursorIndex; oldSelectPos = selectIndex; } public void Undo() { m_Content.text = oldText; cursorIndex = oldPos; selectIndex = oldSelectPos; } public bool Cut() { //Debug.Log ("Cut"); if (isPasswordField) return false; Copy(); return DeleteSelection(); } public void Copy() { //Debug.Log ("Copy"); if (selectIndex == cursorIndex) return; if (isPasswordField) return; string selectedRenderedText = style.Internal_GetSelectedRenderedText(localPosition, m_Content, selectIndex, cursorIndex); GUIUtility.systemCopyBuffer = selectedRenderedText; } internal Rect[] GetHyperlinksRect() { return style.Internal_GetHyperlinksRect(localPosition, m_Content); } static string ReplaceNewlinesWithSpaces(string value) { // First get rid of Windows style new lines and then *nix so we don't leave '\r' around. value = value.Replace("\r\n", " "); value = value.Replace('\n', ' '); // This probably won't happen, but just in case... value = value.Replace('\r', ' '); return value; } public bool Paste() { //Debug.Log ("Paste"); string pasteval = GUIUtility.systemCopyBuffer; if (pasteval != "") { if (!multiline) pasteval = ReplaceNewlinesWithSpaces(pasteval); ReplaceSelection(pasteval); return true; } return false; } static void MapKey(string key, TextEditOp action) { s_Keyactions[Event.KeyboardEvent(key)] = action; } static Dictionary s_Keyactions; /// Set up a platform independant keyboard->Edit action map. This varies depending on whether we are on mac or windows. void InitKeyActions() { if (s_Keyactions != null) return; s_Keyactions = new Dictionary(); // key mappings shared by the platforms MapKey("left", TextEditOp.MoveLeft); MapKey("right", TextEditOp.MoveRight); MapKey("up", TextEditOp.MoveUp); MapKey("down", TextEditOp.MoveDown); MapKey("#left", TextEditOp.SelectLeft); MapKey("#right", TextEditOp.SelectRight); MapKey("#up", TextEditOp.SelectUp); MapKey("#down", TextEditOp.SelectDown); MapKey("delete", TextEditOp.Delete); MapKey("backspace", TextEditOp.Backspace); MapKey("#backspace", TextEditOp.Backspace); // OSX is the special case for input shortcuts if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { // Keyboard mappings for mac // TODO MapKey ("home", TextEditOp.ScrollStart); // TODO MapKey ("end", TextEditOp.ScrollEnd); // TODO MapKey ("page up", TextEditOp.ScrollPageUp); // TODO MapKey ("page down", TextEditOp.ScrollPageDown); MapKey("^left", TextEditOp.MoveGraphicalLineStart); MapKey("^right", TextEditOp.MoveGraphicalLineEnd); // TODO MapKey ("^up", TextEditOp.ScrollPageUp); // TODO MapKey ("^down", TextEditOp.ScrollPageDown); MapKey("&left", TextEditOp.MoveWordLeft); MapKey("&right", TextEditOp.MoveWordRight); MapKey("&up", TextEditOp.MoveParagraphBackward); MapKey("&down", TextEditOp.MoveParagraphForward); MapKey("%left", TextEditOp.MoveGraphicalLineStart); MapKey("%right", TextEditOp.MoveGraphicalLineEnd); MapKey("%up", TextEditOp.MoveTextStart); MapKey("%down", TextEditOp.MoveTextEnd); MapKey("#home", TextEditOp.SelectTextStart); MapKey("#end", TextEditOp.SelectTextEnd); // TODO MapKey ("#page up", TextEditOp.SelectPageUp); // TODO MapKey ("#page down", TextEditOp.SelectPageDown); MapKey("#^left", TextEditOp.ExpandSelectGraphicalLineStart); MapKey("#^right", TextEditOp.ExpandSelectGraphicalLineEnd); MapKey("#^up", TextEditOp.SelectParagraphBackward); MapKey("#^down", TextEditOp.SelectParagraphForward); MapKey("#&left", TextEditOp.SelectWordLeft); MapKey("#&right", TextEditOp.SelectWordRight); MapKey("#&up", TextEditOp.SelectParagraphBackward); MapKey("#&down", TextEditOp.SelectParagraphForward); MapKey("#%left", TextEditOp.ExpandSelectGraphicalLineStart); MapKey("#%right", TextEditOp.ExpandSelectGraphicalLineEnd); MapKey("#%up", TextEditOp.SelectTextStart); MapKey("#%down", TextEditOp.SelectTextEnd); MapKey("%a", TextEditOp.SelectAll); MapKey("%x", TextEditOp.Cut); MapKey("%c", TextEditOp.Copy); MapKey("%v", TextEditOp.Paste); // emacs-like keybindings MapKey("^d", TextEditOp.Delete); MapKey("^h", TextEditOp.Backspace); MapKey("^b", TextEditOp.MoveLeft); MapKey("^f", TextEditOp.MoveRight); MapKey("^a", TextEditOp.MoveLineStart); MapKey("^e", TextEditOp.MoveLineEnd); MapKey("&delete", TextEditOp.DeleteWordForward); MapKey("&backspace", TextEditOp.DeleteWordBack); MapKey("%backspace", TextEditOp.DeleteLineBack); } else { // Windows/Linux keymappings MapKey("home", TextEditOp.MoveGraphicalLineStart); MapKey("end", TextEditOp.MoveGraphicalLineEnd); // TODO MapKey ("page up", TextEditOp.MovePageUp); // TODO MapKey ("page down", TextEditOp.MovePageDown); MapKey("%left", TextEditOp.MoveWordLeft); MapKey("%right", TextEditOp.MoveWordRight); MapKey("%up", TextEditOp.MoveParagraphBackward); MapKey("%down", TextEditOp.MoveParagraphForward); MapKey("^left", TextEditOp.MoveToEndOfPreviousWord); MapKey("^right", TextEditOp.MoveToStartOfNextWord); MapKey("^up", TextEditOp.MoveParagraphBackward); MapKey("^down", TextEditOp.MoveParagraphForward); MapKey("#^left", TextEditOp.SelectToEndOfPreviousWord); MapKey("#^right", TextEditOp.SelectToStartOfNextWord); MapKey("#^up", TextEditOp.SelectParagraphBackward); MapKey("#^down", TextEditOp.SelectParagraphForward); MapKey("#home", TextEditOp.SelectGraphicalLineStart); MapKey("#end", TextEditOp.SelectGraphicalLineEnd); // TODO MapKey ("#page up", TextEditOp.SelectPageUp); // TODO MapKey ("#page down", TextEditOp.SelectPageDown); MapKey("^delete", TextEditOp.DeleteWordForward); MapKey("^backspace", TextEditOp.DeleteWordBack); MapKey("%backspace", TextEditOp.DeleteLineBack); MapKey("^a", TextEditOp.SelectAll); MapKey("^x", TextEditOp.Cut); MapKey("^c", TextEditOp.Copy); MapKey("^v", TextEditOp.Paste); MapKey("#delete", TextEditOp.Cut); MapKey("^insert", TextEditOp.Copy); MapKey("#insert", TextEditOp.Paste); } } public void DetectFocusChange() { OnDetectFocusChange(); } internal virtual void OnDetectFocusChange() { if (m_HasFocus == true && controlID != GUIUtility.keyboardControl) OnLostFocus(); if (m_HasFocus == false && controlID == GUIUtility.keyboardControl) OnFocus(); } internal virtual void OnCursorIndexChange() { } internal virtual void OnSelectIndexChange() { } private void ClampTextIndex(ref int index) { index = Mathf.Clamp(index, 0, text.Length); } void EnsureValidCodePointIndex(ref int index) { ClampTextIndex(ref index); if (!IsValidCodePointIndex(index)) index = NextCodePointIndex(index); } bool IsValidCodePointIndex(int index) { if (index < 0 || index > text.Length) return false; if (index == 0 || index == text.Length) return true; return !char.IsLowSurrogate(text[index]); } int PreviousCodePointIndex(int index) { if (index > 0) index--; while (index > 0 && char.IsLowSurrogate(text[index])) index--; return index; } int NextCodePointIndex(int index) { if (index < text.Length) index++; while (index < text.Length && char.IsLowSurrogate(text[index])) index++; return index; } } } // namespace