diff --git a/Terminal.Gui/Views/TextInput/TextView/PARTIAL-SPLIT-PLAN.md b/Terminal.Gui/Views/TextInput/TextView/PARTIAL-SPLIT-PLAN.md new file mode 100644 index 000000000..e69de29bb diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs new file mode 100644 index 000000000..4a515ac17 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Clipboard.cs @@ -0,0 +1,151 @@ +namespace Terminal.Gui.Views; + +public partial class TextView +{ + private void SetClipboard (string text) + { + if (text is { }) + { + Clipboard.Contents = text; + } + } + /// Copy the selected text to the clipboard contents. + public void Copy () + { + SetWrapModel (); + + if (IsSelecting) + { + _copiedText = GetRegion (out _copiedCellsList); + SetClipboard (_copiedText); + _copyWithoutSelection = false; + } + else + { + List currentLine = GetCurrentLine (); + _copiedCellsList.Add (currentLine); + _copiedText = Cell.ToString (currentLine); + SetClipboard (_copiedText); + _copyWithoutSelection = true; + } + + UpdateWrapModel (); + DoNeededAction (); + } + + /// Cut the selected text to the clipboard contents. + public void Cut () + { + SetWrapModel (); + _copiedText = GetRegion (out _copiedCellsList); + SetClipboard (_copiedText); + + if (!_isReadOnly) + { + ClearRegion (); + + _historyText.Add ( + [new (GetCurrentLine ())], + CursorPosition, + TextEditingLineStatus.Replaced + ); + } + + UpdateWrapModel (); + IsSelecting = false; + DoNeededAction (); + OnContentsChanged (); + } + + /// Paste the clipboard contents into the current selected position. + public void Paste () + { + if (_isReadOnly) + { + return; + } + + SetWrapModel (); + string? contents = Clipboard.Contents; + + if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0) + { + List runeList = contents is null ? [] : Cell.ToCellList (contents); + List currentLine = GetCurrentLine (); + + _historyText.Add ([new (currentLine)], CursorPosition); + + List> addedLine = [new (currentLine), runeList]; + + _historyText.Add ( + [.. addedLine], + CursorPosition, + TextEditingLineStatus.Added + ); + + _model.AddLine (CurrentRow, runeList); + CurrentRow++; + + _historyText.Add ( + [new (GetCurrentLine ())], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + SetNeedsDraw (); + OnContentsChanged (); + } + else + { + if (IsSelecting) + { + ClearRegion (); + } + + _copyWithoutSelection = false; + InsertAllText (contents!, true); + + if (IsSelecting) + { + _historyText.ReplaceLast ( + [new (GetCurrentLine ())], + CursorPosition, + TextEditingLineStatus.Original + ); + } + + SetNeedsDraw (); + } + + UpdateWrapModel (); + IsSelecting = false; + DoNeededAction (); + } + + private void ProcessCopy () + { + ResetColumnTrack (); + Copy (); + } + + private void ProcessCut () + { + ResetColumnTrack (); + Cut (); + } + + private void ProcessPaste () + { + ResetColumnTrack (); + + if (_isReadOnly) + { + return; + } + + Paste (); + } + + + private void AppendClipboard (string text) { Clipboard.Contents += text; } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs new file mode 100644 index 000000000..d535c21cb --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.ContextMenu.cs @@ -0,0 +1,46 @@ +using System.Globalization; + +namespace Terminal.Gui.Views; + +/// Context menu functionality +public partial class TextView +{ + private PopoverMenu CreateContextMenu () + { + PopoverMenu menu = new ( + new List + { + new MenuItem (this, Command.SelectAll, Strings.ctxSelectAll), + new MenuItem (this, Command.DeleteAll, Strings.ctxDeleteAll), + new MenuItem (this, Command.Copy, Strings.ctxCopy), + new MenuItem (this, Command.Cut, Strings.ctxCut), + new MenuItem (this, Command.Paste, Strings.ctxPaste), + new MenuItem (this, Command.Undo, Strings.ctxUndo), + new MenuItem (this, Command.Redo, Strings.ctxRedo) + }); + + menu.KeyChanged += ContextMenu_KeyChanged; + + return menu; + } + + private void ShowContextMenu (Point? mousePosition) + { + if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) + { + _currentCulture = Thread.CurrentThread.CurrentUICulture; + } + + if (mousePosition is null) + { + mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y)); + } + + ContextMenu?.MakeVisible (mousePosition); + } + + private void ContextMenu_KeyChanged (object? sender, KeyChangedEventArgs e) + { + KeyBindings.Replace (e.OldKey, e.NewKey); + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs new file mode 100644 index 000000000..25c08f87b --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Core.cs @@ -0,0 +1,661 @@ +using System.Globalization; + +namespace Terminal.Gui.Views; + +/// Core functionality - Fields, Constructor, and fundamental properties +public partial class TextView +{ + #region Fields + + private readonly HistoryText _historyText = new (); + private bool _allowsReturn = true; + private bool _allowsTab = true; + private bool _clickWithSelecting; + + // The column we are tracking, or -1 if we are not tracking any column + private int _columnTrack = -1; + private bool _continuousFind; + private bool _copyWithoutSelection; + private string? _currentCaller; + private CultureInfo? _currentCulture; + private bool _isButtonShift; + private bool _isButtonReleased; + private bool _isDrawing; + private bool _isReadOnly; + private bool _lastWasKill; + private int _leftColumn; + private TextModel _model = new (); + private bool _multiline = true; + private Dim? _savedHeight; + private int _selectionStartColumn, _selectionStartRow; + private bool _shiftSelecting; + private int _tabWidth = 4; + private int _topRow; + private bool _wordWrap; + private WordWrapManager? _wrapManager; + private bool _wrapNeeded; + + private string? _copiedText; + private List> _copiedCellsList = []; + + #endregion + + #region Constructor + + /// + /// Initializes a on the specified area, with dimensions controlled with the X, Y, Width + /// and Height properties. + /// + public TextView () + { + CanFocus = true; + CursorVisibility = CursorVisibility.Default; + Used = true; + + // By default, disable hotkeys (in case someone sets Title) + base.HotKeySpecifier = new ('\xffff'); + + _model.LinesLoaded += Model_LinesLoaded!; + _historyText.ChangeText += HistoryText_ChangeText!; + + Initialized += TextView_Initialized!; + + SuperViewChanged += TextView_SuperViewChanged!; + + SubViewsLaidOut += TextView_LayoutComplete; + + // Things this view knows how to do + + // Note - NewLine is only bound to Enter if Multiline is true + AddCommand (Command.NewLine, ctx => ProcessEnterKey (ctx)); + + AddCommand ( + Command.PageDown, + () => + { + ProcessPageDown (); + + return true; + } + ); + + AddCommand ( + Command.PageDownExtend, + () => + { + ProcessPageDownExtend (); + + return true; + } + ); + + AddCommand ( + Command.PageUp, + () => + { + ProcessPageUp (); + + return true; + } + ); + + AddCommand ( + Command.PageUpExtend, + () => + { + ProcessPageUpExtend (); + + return true; + } + ); + + AddCommand (Command.Down, () => ProcessMoveDown ()); + + AddCommand ( + Command.DownExtend, + () => + { + ProcessMoveDownExtend (); + + return true; + } + ); + + AddCommand (Command.Up, () => ProcessMoveUp ()); + + AddCommand ( + Command.UpExtend, + () => + { + ProcessMoveUpExtend (); + + return true; + } + ); + AddCommand (Command.Right, () => ProcessMoveRight ()); + + AddCommand ( + Command.RightExtend, + () => + { + ProcessMoveRightExtend (); + + return true; + } + ); + AddCommand (Command.Left, () => ProcessMoveLeft ()); + + AddCommand ( + Command.LeftExtend, + () => + { + ProcessMoveLeftExtend (); + + return true; + } + ); + + AddCommand ( + Command.DeleteCharLeft, + () => + { + ProcessDeleteCharLeft (); + + return true; + } + ); + + AddCommand ( + Command.LeftStart, + () => + { + ProcessMoveLeftStart (); + + return true; + } + ); + + AddCommand ( + Command.LeftStartExtend, + () => + { + ProcessMoveLeftStartExtend (); + + return true; + } + ); + + AddCommand ( + Command.DeleteCharRight, + () => + { + ProcessDeleteCharRight (); + + return true; + } + ); + + AddCommand ( + Command.RightEnd, + () => + { + ProcessMoveEndOfLine (); + + return true; + } + ); + + AddCommand ( + Command.RightEndExtend, + () => + { + ProcessMoveRightEndExtend (); + + return true; + } + ); + + AddCommand ( + Command.CutToEndLine, + () => + { + KillToEndOfLine (); + + return true; + } + ); + + AddCommand ( + Command.CutToStartLine, + () => + { + KillToLeftStart (); + + return true; + } + ); + + AddCommand ( + Command.Paste, + () => + { + ProcessPaste (); + + return true; + } + ); + + AddCommand ( + Command.ToggleExtend, + () => + { + ToggleSelecting (); + + return true; + } + ); + + AddCommand ( + Command.Copy, + () => + { + ProcessCopy (); + + return true; + } + ); + + AddCommand ( + Command.Cut, + () => + { + ProcessCut (); + + return true; + } + ); + + AddCommand ( + Command.WordLeft, + () => + { + ProcessMoveWordBackward (); + + return true; + } + ); + + AddCommand ( + Command.WordLeftExtend, + () => + { + ProcessMoveWordBackwardExtend (); + + return true; + } + ); + + AddCommand ( + Command.WordRight, + () => + { + ProcessMoveWordForward (); + + return true; + } + ); + + AddCommand ( + Command.WordRightExtend, + () => + { + ProcessMoveWordForwardExtend (); + + return true; + } + ); + + AddCommand ( + Command.KillWordForwards, + () => + { + ProcessKillWordForward (); + + return true; + } + ); + + AddCommand ( + Command.KillWordBackwards, + () => + { + ProcessKillWordBackward (); + + return true; + } + ); + + AddCommand ( + Command.End, + () => + { + MoveBottomEnd (); + + return true; + } + ); + + AddCommand ( + Command.EndExtend, + () => + { + MoveBottomEndExtend (); + + return true; + } + ); + + AddCommand ( + Command.Start, + () => + { + MoveTopHome (); + + return true; + } + ); + + AddCommand ( + Command.StartExtend, + () => + { + MoveTopHomeExtend (); + + return true; + } + ); + + AddCommand ( + Command.SelectAll, + () => + { + ProcessSelectAll (); + + return true; + } + ); + + AddCommand ( + Command.ToggleOverwrite, + () => + { + ProcessSetOverwrite (); + + return true; + } + ); + + AddCommand ( + Command.EnableOverwrite, + () => + { + SetOverwrite (true); + + return true; + } + ); + + AddCommand ( + Command.DisableOverwrite, + () => + { + SetOverwrite (false); + + return true; + } + ); + AddCommand (Command.Tab, () => ProcessTab ()); + AddCommand (Command.BackTab, () => ProcessBackTab ()); + + AddCommand ( + Command.Undo, + () => + { + Undo (); + + return true; + } + ); + + AddCommand ( + Command.Redo, + () => + { + Redo (); + + return true; + } + ); + + AddCommand ( + Command.DeleteAll, + () => + { + DeleteAll (); + + return true; + } + ); + + AddCommand ( + Command.Context, + () => + { + ShowContextMenu (null); + + return true; + } + ); + + AddCommand ( + Command.Open, + () => + { + PromptForColors (); + + return true; + }); + + // Default keybindings for this view + KeyBindings.Remove (Key.Space); + + KeyBindings.Remove (Key.Enter); + KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept); + + KeyBindings.Add (Key.PageDown, Command.PageDown); + KeyBindings.Add (Key.V.WithCtrl, Command.PageDown); + + KeyBindings.Add (Key.PageDown.WithShift, Command.PageDownExtend); + + KeyBindings.Add (Key.PageUp, Command.PageUp); + + KeyBindings.Add (Key.PageUp.WithShift, Command.PageUpExtend); + + KeyBindings.Add (Key.N.WithCtrl, Command.Down); + KeyBindings.Add (Key.CursorDown, Command.Down); + + KeyBindings.Add (Key.CursorDown.WithShift, Command.DownExtend); + + KeyBindings.Add (Key.P.WithCtrl, Command.Up); + KeyBindings.Add (Key.CursorUp, Command.Up); + + KeyBindings.Add (Key.CursorUp.WithShift, Command.UpExtend); + + KeyBindings.Add (Key.F.WithCtrl, Command.Right); + KeyBindings.Add (Key.CursorRight, Command.Right); + + KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend); + + KeyBindings.Add (Key.B.WithCtrl, Command.Left); + KeyBindings.Add (Key.CursorLeft, Command.Left); + + KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend); + + KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + + KeyBindings.Add (Key.Home, Command.LeftStart); + + KeyBindings.Add (Key.Home.WithShift, Command.LeftStartExtend); + + KeyBindings.Add (Key.Delete, Command.DeleteCharRight); + KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); + + KeyBindings.Add (Key.End, Command.RightEnd); + KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); + + KeyBindings.Add (Key.End.WithShift, Command.RightEndExtend); + + KeyBindings.Add (Key.K.WithCtrl, Command.CutToEndLine); // kill-to-end + + KeyBindings.Add (Key.Delete.WithCtrl.WithShift, Command.CutToEndLine); // kill-to-end + + KeyBindings.Add (Key.Backspace.WithCtrl.WithShift, Command.CutToStartLine); // kill-to-start + + KeyBindings.Add (Key.Y.WithCtrl, Command.Paste); // Control-y, yank + KeyBindings.Add (Key.Space.WithCtrl, Command.ToggleExtend); + + KeyBindings.Add (Key.C.WithCtrl, Command.Copy); + + KeyBindings.Add (Key.W.WithCtrl, Command.Cut); // Move to Unix? + KeyBindings.Add (Key.X.WithCtrl, Command.Cut); + + KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.WordLeft); + + KeyBindings.Add (Key.CursorLeft.WithCtrl.WithShift, Command.WordLeftExtend); + + KeyBindings.Add (Key.CursorRight.WithCtrl, Command.WordRight); + + KeyBindings.Add (Key.CursorRight.WithCtrl.WithShift, Command.WordRightExtend); + KeyBindings.Add (Key.Delete.WithCtrl, Command.KillWordForwards); // kill-word-forwards + KeyBindings.Add (Key.Backspace.WithCtrl, Command.KillWordBackwards); // kill-word-backwards + + KeyBindings.Add (Key.End.WithCtrl, Command.End); + KeyBindings.Add (Key.End.WithCtrl.WithShift, Command.EndExtend); + KeyBindings.Add (Key.Home.WithCtrl, Command.Start); + KeyBindings.Add (Key.Home.WithCtrl.WithShift, Command.StartExtend); + KeyBindings.Add (Key.A.WithCtrl, Command.SelectAll); + KeyBindings.Add (Key.InsertChar, Command.ToggleOverwrite); + KeyBindings.Add (Key.Tab, Command.Tab); + KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); + + KeyBindings.Add (Key.Z.WithCtrl, Command.Undo); + KeyBindings.Add (Key.R.WithCtrl, Command.Redo); + + KeyBindings.Add (Key.G.WithCtrl, Command.DeleteAll); + KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll); + + KeyBindings.Add (Key.L.WithCtrl, Command.Open); + +#if UNIX_KEY_BINDINGS + KeyBindings.Add (Key.C.WithAlt, Command.Copy); + KeyBindings.Add (Key.B.WithAlt, Command.WordLeft); + KeyBindings.Add (Key.W.WithAlt, Command.Cut); + KeyBindings.Add (Key.V.WithAlt, Command.PageUp); + KeyBindings.Add (Key.F.WithAlt, Command.WordRight); + KeyBindings.Add (Key.K.WithAlt, Command.CutToStartLine); // kill-to-start +#endif + + _currentCulture = Thread.CurrentThread.CurrentUICulture; + } + + #endregion + + #region Initialization and Configuration + + /// + /// Configures the ScrollBars to work with the modern View scrolling system. + /// + private void ConfigureScrollBars () + { + // Subscribe to ViewportChanged to sync internal scroll fields + ViewportChanged += TextView_ViewportChanged; + + // Vertical ScrollBar: AutoShow enabled by default as per requirements + VerticalScrollBar.AutoShow = true; + + // Horizontal ScrollBar: AutoShow tracks WordWrap as per requirements + HorizontalScrollBar.AutoShow = !WordWrap; + } + + private void TextView_Initialized (object sender, EventArgs e) + { + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + } + + ContextMenu = CreateContextMenu (); + App?.Popover?.Register (ContextMenu); + KeyBindings.Add (ContextMenu.Key, Command.Context); + + // Configure ScrollBars to use modern View scrolling infrastructure + ConfigureScrollBars (); + + OnContentsChanged (); + } + + private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e) + { + if (e.SuperView is { }) + { + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + } + } + else + { + Autocomplete.HostControl = null; + } + } + + private void TextView_ViewportChanged (object? sender, DrawEventArgs e) + { + // Sync internal scroll position fields with Viewport + // Only update if values actually changed to prevent infinite loops + if (_topRow != Viewport.Y) + { + _topRow = Viewport.Y; + } + + if (_leftColumn != Viewport.X) + { + _leftColumn = Viewport.X; + } + } + + private void TextView_LayoutComplete (object? sender, LayoutEventArgs e) + { + WrapTextModel (); + UpdateContentSize (); + Adjust (); + } + + private void Model_LinesLoaded (object sender, EventArgs e) + { + // This call is not needed. Model_LinesLoaded gets invoked when + // model.LoadString (value) is called. LoadString is called from one place + // (Text.set) and historyText.Clear() is called immediately after. + // If this call happens, HistoryText_ChangeText will get called multiple times + // when Text is set, which is wrong. + //historyText.Clear (Text); + + if (!_multiline && !IsInitialized) + { + CurrentColumn = Text.GetRuneCount (); + _leftColumn = CurrentColumn > Viewport.Width + 1 ? CurrentColumn - Viewport.Width + 1 : 0; + } + } + + #endregion +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs new file mode 100644 index 000000000..01fb83e71 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Delete.cs @@ -0,0 +1,641 @@ +namespace Terminal.Gui.Views; + +public partial class TextView +{ + /// Deletes all text. + public void DeleteAll () + { + if (Lines == 0) + { + return; + } + + _selectionStartColumn = 0; + _selectionStartRow = 0; + MoveBottomEndExtend (); + DeleteCharLeft (); + SetNeedsDraw (); + } + + /// Deletes all the selected or a single character at left from the position of the cursor. + public void DeleteCharLeft () + { + if (_isReadOnly) + { + return; + } + + SetWrapModel (); + + if (IsSelecting) + { + _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition); + + ClearSelectedRegion (); + + List currentLine = GetCurrentLine (); + + _historyText.Add ( + new () { new (currentLine) }, + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + OnContentsChanged (); + + return; + } + + if (DeleteTextBackwards ()) + { + UpdateWrapModel (); + OnContentsChanged (); + + return; + } + + UpdateWrapModel (); + + DoNeededAction (); + OnContentsChanged (); + } + + /// Deletes all the selected or a single character at right from the position of the cursor. + public void DeleteCharRight () + { + if (_isReadOnly) + { + return; + } + + SetWrapModel (); + + if (IsSelecting) + { + _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition); + + ClearSelectedRegion (); + + List currentLine = GetCurrentLine (); + + _historyText.Add ( + new () { new (currentLine) }, + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + OnContentsChanged (); + + return; + } + + if (DeleteTextForwards ()) + { + UpdateWrapModel (); + OnContentsChanged (); + + return; + } + + UpdateWrapModel (); + + DoNeededAction (); + OnContentsChanged (); + } + + private bool DeleteTextBackwards () + { + SetWrapModel (); + + if (CurrentColumn > 0) + { + // Delete backwards + List currentLine = GetCurrentLine (); + + _historyText.Add (new () { new (currentLine) }, CursorPosition); + + currentLine.RemoveAt (CurrentColumn - 1); + + if (_wordWrap) + { + _wrapNeeded = true; + } + + CurrentColumn--; + + _historyText.Add ( + new () { new (currentLine) }, + CursorPosition, + TextEditingLineStatus.Replaced + ); + + if (CurrentColumn < _leftColumn) + { + _leftColumn--; + SetNeedsDraw (); + } + else + { + // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. + //SetNeedsDraw (new (0, currentRow - topRow, 1, Viewport.Width)); + SetNeedsDraw (); + } + } + else + { + // Merges the current line with the previous one. + if (CurrentRow == 0) + { + return true; + } + + int prowIdx = CurrentRow - 1; + List prevRow = _model.GetLine (prowIdx); + + _historyText.Add (new () { new (prevRow) }, CursorPosition); + + List> removedLines = new () { new (prevRow) }; + + removedLines.Add (new (GetCurrentLine ())); + + _historyText.Add ( + removedLines, + new (CurrentColumn, prowIdx), + TextEditingLineStatus.Removed + ); + + int prevCount = prevRow.Count; + _model.GetLine (prowIdx).AddRange (GetCurrentLine ()); + _model.RemoveLine (CurrentRow); + + if (_wordWrap) + { + _wrapNeeded = true; + } + + CurrentRow--; + + _historyText.Add ( + new () { GetCurrentLine () }, + new (CurrentColumn, prowIdx), + TextEditingLineStatus.Replaced + ); + + CurrentColumn = prevCount; + SetNeedsDraw (); + } + + UpdateWrapModel (); + + return false; + } + + private bool DeleteTextForwards () + { + SetWrapModel (); + + List currentLine = GetCurrentLine (); + + if (CurrentColumn == currentLine.Count) + { + if (CurrentRow + 1 == _model.Count) + { + UpdateWrapModel (); + + return true; + } + + _historyText.Add (new () { new (currentLine) }, CursorPosition); + + List> removedLines = new () { new (currentLine) }; + + List nextLine = _model.GetLine (CurrentRow + 1); + + removedLines.Add (new (nextLine)); + + _historyText.Add (removedLines, CursorPosition, TextEditingLineStatus.Removed); + + currentLine.AddRange (nextLine); + _model.RemoveLine (CurrentRow + 1); + + _historyText.Add ( + new () { new (currentLine) }, + CursorPosition, + TextEditingLineStatus.Replaced + ); + + if (_wordWrap) + { + _wrapNeeded = true; + } + + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1)); + } + else + { + _historyText.Add ([ [.. currentLine]], CursorPosition); + + currentLine.RemoveAt (CurrentColumn); + + _historyText.Add ( + [ [.. currentLine]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + if (_wordWrap) + { + _wrapNeeded = true; + } + + DoSetNeedsDraw ( + new ( + CurrentColumn - _leftColumn, + CurrentRow - _topRow, + Viewport.Width, + Math.Max (CurrentRow - _topRow + 1, 0) + ) + ); + } + + UpdateWrapModel (); + + return false; + } + + private void ProcessKillWordForward () + { + ResetColumnTrack (); + StopSelecting (); + KillWordForward (); + } + + private void ProcessKillWordBackward () + { + ResetColumnTrack (); + KillWordBackward (); + } + + private void ProcessDeleteCharRight () + { + ResetColumnTrack (); + DeleteCharRight (); + } + + private void ProcessDeleteCharLeft () + { + ResetColumnTrack (); + DeleteCharLeft (); + } + + private void KillWordForward () + { + if (_isReadOnly) + { + return; + } + + SetWrapModel (); + + List currentLine = GetCurrentLine (); + + _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); + + if (currentLine.Count == 0 || CurrentColumn == currentLine.Count) + { + DeleteTextForwards (); + + _historyText.ReplaceLast ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + return; + } + + (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); + var restCount = 0; + + if (newPos.HasValue && CurrentRow == newPos.Value.row) + { + restCount = newPos.Value.col - CurrentColumn; + currentLine.RemoveRange (CurrentColumn, restCount); + } + else if (newPos.HasValue) + { + restCount = currentLine.Count - CurrentColumn; + currentLine.RemoveRange (CurrentColumn, restCount); + } + + if (_wordWrap) + { + _wrapNeeded = true; + } + + _historyText.Add ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoNeededAction (); + } + + private void KillWordBackward () + { + if (_isReadOnly) + { + return; + } + + SetWrapModel (); + + List currentLine = GetCurrentLine (); + + _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); + + if (CurrentColumn == 0) + { + DeleteTextBackwards (); + + _historyText.ReplaceLast ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + return; + } + + (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); + + if (newPos.HasValue && CurrentRow == newPos.Value.row) + { + int restCount = CurrentColumn - newPos.Value.col; + currentLine.RemoveRange (newPos.Value.col, restCount); + + if (_wordWrap) + { + _wrapNeeded = true; + } + + CurrentColumn = newPos.Value.col; + } + else if (newPos.HasValue) + { + int restCount; + + if (newPos.Value.row == CurrentRow) + { + restCount = currentLine.Count - CurrentColumn; + currentLine.RemoveRange (CurrentColumn, restCount); + } + else + { + while (CurrentRow != newPos.Value.row) + { + restCount = currentLine.Count; + currentLine.RemoveRange (0, restCount); + + CurrentRow--; + currentLine = GetCurrentLine (); + } + } + + if (_wordWrap) + { + _wrapNeeded = true; + } + + CurrentColumn = newPos.Value.col; + CurrentRow = newPos.Value.row; + } + + _historyText.Add ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoNeededAction (); + } + + private void KillToLeftStart () + { + if (_isReadOnly) + { + return; + } + + if (_model.Count == 1 && GetCurrentLine ().Count == 0) + { + // Prevents from adding line feeds if there is no more lines. + return; + } + + SetWrapModel (); + + List currentLine = GetCurrentLine (); + var setLastWasKill = true; + + if (currentLine.Count > 0 && CurrentColumn == 0) + { + UpdateWrapModel (); + + DeleteTextBackwards (); + + return; + } + + _historyText.Add ([ [.. currentLine]], CursorPosition); + + if (currentLine.Count == 0) + { + if (CurrentRow > 0) + { + _model.RemoveLine (CurrentRow); + + if (_model.Count > 0 || _lastWasKill) + { + string val = Environment.NewLine; + + if (_lastWasKill) + { + AppendClipboard (val); + } + else + { + SetClipboard (val); + } + } + + if (_model.Count == 0) + { + // Prevents from adding line feeds if there is no more lines. + setLastWasKill = false; + } + + CurrentRow--; + currentLine = _model.GetLine (CurrentRow); + + List> removedLine = + [ + [..currentLine], + [] + ]; + + _historyText.Add ( + [.. removedLine], + CursorPosition, + TextEditingLineStatus.Removed + ); + + CurrentColumn = currentLine.Count; + } + } + else + { + int restCount = CurrentColumn; + List rest = currentLine.GetRange (0, restCount); + var val = string.Empty; + val += StringFromCells (rest); + + if (_lastWasKill) + { + AppendClipboard (val); + } + else + { + SetClipboard (val); + } + + currentLine.RemoveRange (0, restCount); + CurrentColumn = 0; + } + + _historyText.Add ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + + _lastWasKill = setLastWasKill; + DoNeededAction (); + } + + private void KillToEndOfLine () + { + if (_isReadOnly) + { + return; + } + + if (_model.Count == 1 && GetCurrentLine ().Count == 0) + { + // Prevents from adding line feeds if there is no more lines. + return; + } + + SetWrapModel (); + + List currentLine = GetCurrentLine (); + var setLastWasKill = true; + + if (currentLine.Count > 0 && CurrentColumn == currentLine.Count) + { + UpdateWrapModel (); + + DeleteTextForwards (); + + return; + } + + _historyText.Add (new () { new (currentLine) }, CursorPosition); + + if (currentLine.Count == 0) + { + if (CurrentRow < _model.Count - 1) + { + List> removedLines = new () { new (currentLine) }; + + _model.RemoveLine (CurrentRow); + + removedLines.Add (new (GetCurrentLine ())); + + _historyText.Add ( + new (removedLines), + CursorPosition, + TextEditingLineStatus.Removed + ); + } + + if (_model.Count > 0 || _lastWasKill) + { + string val = Environment.NewLine; + + if (_lastWasKill) + { + AppendClipboard (val); + } + else + { + SetClipboard (val); + } + } + + if (_model.Count == 0) + { + // Prevents from adding line feeds if there is no more lines. + setLastWasKill = false; + } + } + else + { + int restCount = currentLine.Count - CurrentColumn; + List rest = currentLine.GetRange (CurrentColumn, restCount); + var val = string.Empty; + val += StringFromCells (rest); + + if (_lastWasKill) + { + AppendClipboard (val); + } + else + { + SetClipboard (val); + } + + currentLine.RemoveRange (CurrentColumn, restCount); + } + + _historyText.Add ( + [ [.. GetCurrentLine ()]], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + + _lastWasKill = setLastWasKill; + DoNeededAction (); + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs new file mode 100644 index 000000000..24dff6c2c --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs @@ -0,0 +1,348 @@ +namespace Terminal.Gui.Views; + +public partial class TextView +{ + internal void ApplyCellsAttribute (Attribute attribute) + { + if (!ReadOnly && SelectedLength > 0) + { + int startRow = Math.Min (SelectionStartRow, CurrentRow); + int endRow = Math.Max (CurrentRow, SelectionStartRow); + int startCol = SelectionStartRow <= CurrentRow ? SelectionStartColumn : CurrentColumn; + int endCol = CurrentRow >= SelectionStartRow ? CurrentColumn : SelectionStartColumn; + List> selectedCellsOriginal = []; + List> selectedCellsChanged = []; + + for (int r = startRow; r <= endRow; r++) + { + List line = GetLine (r); + + selectedCellsOriginal.Add ([.. line]); + + for (int c = r == startRow ? startCol : 0; + c < (r == endRow ? endCol : line.Count); + c++) + { + Cell cell = line [c]; // Copy value to a new variable + cell.Attribute = attribute; // Modify the copy + line [c] = cell; // Assign the modified copy back + } + + selectedCellsChanged.Add ([.. GetLine (r)]); + } + + GetSelectedRegion (); + IsSelecting = false; + + _historyText.Add ( + [.. selectedCellsOriginal], + new Point (startCol, startRow) + ); + + _historyText.Add ( + [.. selectedCellsChanged], + new Point (startCol, startRow), + TextEditingLineStatus.Attribute + ); + } + } + + private Attribute? GetSelectedCellAttribute () + { + List line; + + if (SelectedLength > 0) + { + line = GetLine (SelectionStartRow); + + if (line [Math.Min (SelectionStartColumn, line.Count - 1)].Attribute is { } attributeSel) + { + return new (attributeSel); + } + + return GetAttributeForRole (VisualRole.Active); + } + + line = GetCurrentLine (); + + if (line [Math.Min (CurrentColumn, line.Count - 1)].Attribute is { } attribute) + { + return new (attribute); + } + + return GetAttributeForRole (VisualRole.Active); + } + + /// Invoked when the normal color is drawn. + public event EventHandler? DrawNormalColor; + + /// Invoked when the ready only color is drawn. + public event EventHandler? DrawReadOnlyColor; + + /// Invoked when the selection color is drawn. + public event EventHandler? DrawSelectionColor; + + /// + /// Invoked when the used color is drawn. The Used Color is used to indicate if the + /// was pressed and enabled. + /// + public event EventHandler? DrawUsedColor; + + /// + protected override bool OnDrawingContent () + { + _isDrawing = true; + + SetAttributeForRole (Enabled ? VisualRole.Editable : VisualRole.Disabled); + + (int width, int height) offB = OffSetBackground (); + int right = Viewport.Width + offB.width; + int bottom = Viewport.Height + offB.height; + var row = 0; + + for (int idxRow = _topRow; idxRow < _model.Count; idxRow++) + { + List line = _model.GetLine (idxRow); + int lineRuneCount = line.Count; + var col = 0; + + Move (0, row); + + for (int idxCol = _leftColumn; idxCol < lineRuneCount; idxCol++) + { + string text = idxCol >= lineRuneCount ? " " : line [idxCol].Grapheme; + int cols = text.GetColumns (false); + + if (idxCol < line.Count && IsSelecting && PointInSelection (idxCol, idxRow)) + { + OnDrawSelectionColor (line, idxCol, idxRow); + } + else if (idxCol == CurrentColumn && idxRow == CurrentRow && !IsSelecting && !Used && HasFocus && idxCol < lineRuneCount) + { + OnDrawUsedColor (line, idxCol, idxRow); + } + else if (ReadOnly) + { + OnDrawReadOnlyColor (line, idxCol, idxRow); + } + else + { + OnDrawNormalColor (line, idxCol, idxRow); + } + + if (text == "\t") + { + cols += TabWidth + 1; + + if (col + cols > right) + { + cols = right - col; + } + + for (var i = 0; i < cols; i++) + { + if (col + i < right) + { + AddRune (col + i, row, (Rune)' '); + } + } + } + else + { + AddStr (col, row, text); + + // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune + cols = Math.Max (cols, 1); + } + + if (!TextModel.SetCol (ref col, Viewport.Right, cols)) + { + break; + } + + if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Grapheme.GetColumns () > right) + { + break; + } + } + + if (col < right) + { + SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable); + ClearRegion (col, row, right, row + 1); + } + + row++; + } + + if (row < bottom) + { + SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable); + ClearRegion (Viewport.Left, row, right, bottom); + } + + _isDrawing = false; + + return false; + } + + /// + /// Sets the to an appropriate color for rendering the given + /// of the current . Override to provide custom coloring by calling + /// Defaults to . + /// + /// The line. + /// The col index. + /// The row index. + protected virtual void OnDrawNormalColor (List line, int idxCol, int idxRow) + { + (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); + var ev = new CellEventArgs (line, idxCol, unwrappedPos); + DrawNormalColor?.Invoke (this, ev); + + if (line [idxCol].Attribute is { }) + { + Attribute? attribute = line [idxCol].Attribute; + SetAttribute ((Attribute)attribute!); + } + else + { + SetAttribute (GetAttributeForRole (VisualRole.Normal)); + } + } + + /// + /// Sets the to an appropriate color for rendering the given + /// of the current . Override to provide custom coloring by calling + /// Defaults to . + /// + /// The line. + /// The col index. + /// /// + /// The row index. + protected virtual void OnDrawReadOnlyColor (List line, int idxCol, int idxRow) + { + (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); + var ev = new CellEventArgs (line, idxCol, unwrappedPos); + DrawReadOnlyColor?.Invoke (this, ev); + + Attribute? cellAttribute = line [idxCol].Attribute is { } ? line [idxCol].Attribute : GetAttributeForRole (VisualRole.ReadOnly); + + if (cellAttribute!.Value.Foreground == cellAttribute.Value.Background) + { + SetAttribute (new (cellAttribute.Value.Foreground, cellAttribute.Value.Background, cellAttribute.Value.Style)); + } + else + { + SetAttributeForRole (VisualRole.ReadOnly); + } + } + + /// + /// Sets the to an appropriate color for rendering the given + /// of the current . Override to provide custom coloring by calling + /// Defaults to . + /// + /// The line. + /// The col index. + /// /// + /// The row index. + protected virtual void OnDrawSelectionColor (List line, int idxCol, int idxRow) + { + (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); + var ev = new CellEventArgs (line, idxCol, unwrappedPos); + DrawSelectionColor?.Invoke (this, ev); + + if (line [idxCol].Attribute is { }) + { + Attribute? attribute = line [idxCol].Attribute; + Attribute? active = GetAttributeForRole (VisualRole.Active); + SetAttribute (new (active!.Value.Foreground, active.Value.Background, attribute!.Value.Style)); + } + else + { + SetAttributeForRole (VisualRole.Active); + } + } + + /// + /// Sets the to an appropriate color for rendering the given + /// of the current . Override to provide custom coloring by calling + /// Defaults to . + /// + /// The line. + /// The col index. + /// /// + /// The row index. + protected virtual void OnDrawUsedColor (List line, int idxCol, int idxRow) + { + (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); + var ev = new CellEventArgs (line, idxCol, unwrappedPos); + DrawUsedColor?.Invoke (this, ev); + + if (line [idxCol].Attribute is { }) + { + Attribute? attribute = line [idxCol].Attribute; + SetValidUsedColor (attribute!); + } + else + { + SetValidUsedColor (GetAttributeForRole (VisualRole.Focus)); + } + } + + private void DoSetNeedsDraw (Rectangle rect) + { + if (_wrapNeeded) + { + SetNeedsDraw (); + } + else + { + // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. + //SetNeedsDraw (rect); + SetNeedsDraw (); + } + } + + private Attribute? GetSelectedAttribute (int row, int col) + { + if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0)) + { + return null; + } + + List line = GetLine (row); + int foundRow = row; + + while (line.Count == 0) + { + if (foundRow == 0 && line.Count == 0) + { + return null; + } + + foundRow--; + line = GetLine (foundRow); + } + + int foundCol = foundRow < row ? line.Count - 1 : Math.Min (col, line.Count - 1); + + Cell cell = line [foundCol]; + + return cell.Attribute; + } + + /// + protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute) + { + if (role == VisualRole.Normal) + { + currentAttribute = GetAttributeForRole (VisualRole.Editable); + + return true; + } + + return base.OnGettingAttributeForRole (role, ref currentAttribute); + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs new file mode 100644 index 000000000..5d0650242 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Find.cs @@ -0,0 +1,210 @@ +namespace Terminal.Gui.Views; + +/// Find and Replace functionality +public partial class TextView +{ + #region Public Find/Replace Methods + + /// Find the next text based on the match case with the option to replace it. + /// The text to find. + /// trueIf all the text was forward searched.falseotherwise. + /// The match case setting. + /// The match whole word setting. + /// The text to replace. + /// trueIf is replacing.falseotherwise. + /// trueIf the text was found.falseotherwise. + public bool FindNextText ( + string textToFind, + out bool gaveFullTurn, + bool matchCase = false, + bool matchWholeWord = false, + string? textToReplace = null, + bool replace = false + ) + { + if (_model.Count == 0) + { + gaveFullTurn = false; + + return false; + } + + SetWrapModel (); + ResetContinuousFind (); + + (Point current, bool found) foundPos = + _model.FindNextText (textToFind, out gaveFullTurn, matchCase, matchWholeWord); + + return SetFoundText (textToFind, foundPos, textToReplace, replace); + } + + /// Find the previous text based on the match case with the option to replace it. + /// The text to find. + /// trueIf all the text was backward searched.falseotherwise. + /// The match case setting. + /// The match whole word setting. + /// The text to replace. + /// trueIf the text was found.falseotherwise. + /// trueIf the text was found.falseotherwise. + public bool FindPreviousText ( + string textToFind, + out bool gaveFullTurn, + bool matchCase = false, + bool matchWholeWord = false, + string? textToReplace = null, + bool replace = false + ) + { + if (_model.Count == 0) + { + gaveFullTurn = false; + + return false; + } + + SetWrapModel (); + ResetContinuousFind (); + + (Point current, bool found) foundPos = + _model.FindPreviousText (textToFind, out gaveFullTurn, matchCase, matchWholeWord); + + return SetFoundText (textToFind, foundPos, textToReplace, replace); + } + + /// Reset the flag to stop continuous find. + public void FindTextChanged () { _continuousFind = false; } + + /// Replaces all the text based on the match case. + /// The text to find. + /// The match case setting. + /// The match whole word setting. + /// The text to replace. + /// trueIf the text was found.falseotherwise. + public bool ReplaceAllText ( + string textToFind, + bool matchCase = false, + bool matchWholeWord = false, + string? textToReplace = null + ) + { + if (_isReadOnly || _model.Count == 0) + { + return false; + } + + SetWrapModel (); + ResetContinuousFind (); + + (Point current, bool found) foundPos = + _model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace); + + return SetFoundText (textToFind, foundPos, textToReplace, false, true); + } + + #endregion + + #region Private Find Helper Methods + + private void ResetContinuousFind () + { + if (!_continuousFind) + { + int col = IsSelecting ? _selectionStartColumn : CurrentColumn; + int row = IsSelecting ? _selectionStartRow : CurrentRow; + _model.ResetContinuousFind (new (col, row)); + } + } + + private void ResetContinuousFindTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + _lastWasKill = false; + _continuousFind = false; + } + + private bool SetFoundText ( + string text, + (Point current, bool found) foundPos, + string? textToReplace = null, + bool replace = false, + bool replaceAll = false + ) + { + if (foundPos.found) + { + StartSelecting (); + _selectionStartColumn = foundPos.current.X; + _selectionStartRow = foundPos.current.Y; + + if (!replaceAll) + { + CurrentColumn = _selectionStartColumn + text.GetRuneCount (); + } + else + { + CurrentColumn = _selectionStartColumn + textToReplace!.GetRuneCount (); + } + + CurrentRow = foundPos.current.Y; + + if (!_isReadOnly && replace) + { + Adjust (); + ClearSelectedRegion (); + InsertAllText (textToReplace!); + StartSelecting (); + _selectionStartColumn = CurrentColumn - textToReplace!.GetRuneCount (); + } + else + { + UpdateWrapModel (); + SetNeedsDraw (); + Adjust (); + } + + _continuousFind = true; + + return foundPos.found; + } + + UpdateWrapModel (); + _continuousFind = false; + + return foundPos.found; + } + + private IEnumerable<(int col, int row, Cell rune)> ForwardIterator (int col, int row) + { + if (col < 0 || row < 0) + { + yield break; + } + + if (row >= _model.Count) + { + yield break; + } + + List line = GetCurrentLine (); + + if (col >= line.Count) + { + yield break; + } + + while (row < _model.Count) + { + for (int c = col; c < line.Count; c++) + { + yield return (c, row, line [c]); + } + + col = 0; + row++; + line = GetCurrentLine (); + } + } + + #endregion +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs new file mode 100644 index 000000000..136487483 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Insert.cs @@ -0,0 +1,309 @@ +namespace Terminal.Gui.Views; + +public partial class TextView +{ + /// + /// Inserts the given text at the current cursor position exactly as if the user had just + /// typed it + /// + /// Text to add + public void InsertText (string toAdd) + { + foreach (char ch in toAdd) + { + Key key; + + try + { + key = new (ch); + } + catch (Exception) + { + throw new ArgumentException ( + $"Cannot insert character '{ch}' because it does not map to a Key" + ); + } + + InsertText (key); + + if (NeedsDraw) + { + Adjust (); + } + else + { + PositionCursor (); + } + } + } + + private void Insert (Cell cell) + { + List line = GetCurrentLine (); + + if (Used) + { + line.Insert (Math.Min (CurrentColumn, line.Count), cell); + } + else + { + if (CurrentColumn < line.Count) + { + line.RemoveAt (CurrentColumn); + } + + line.Insert (Math.Min (CurrentColumn, line.Count), cell); + } + + int prow = CurrentRow - _topRow; + + if (!_wrapNeeded) + { + // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. + //SetNeedsDraw (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0))); + SetNeedsDraw (); + } + } + + private void InsertAllText (string text, bool fromClipboard = false) + { + if (string.IsNullOrEmpty (text)) + { + return; + } + + List> lines; + + if (fromClipboard && text == _copiedText) + { + lines = _copiedCellsList; + } + else + { + // Get selected attribute + Attribute? attribute = GetSelectedAttribute (CurrentRow, CurrentColumn); + lines = Cell.StringToLinesOfCells (text, attribute); + } + + if (lines.Count == 0) + { + return; + } + + SetWrapModel (); + + List line = GetCurrentLine (); + + _historyText.Add ([new (line)], CursorPosition); + + // Optimize single line + if (lines.Count == 1) + { + line.InsertRange (CurrentColumn, lines [0]); + CurrentColumn += lines [0].Count; + + _historyText.Add ( + [new (line)], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + if (!_wordWrap && CurrentColumn - _leftColumn > Viewport.Width) + { + _leftColumn = Math.Max (CurrentColumn - Viewport.Width + 1, 0); + } + + if (_wordWrap) + { + SetNeedsDraw (); + } + else + { + // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. + //SetNeedsDraw (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0))); + SetNeedsDraw (); + } + + UpdateWrapModel (); + + OnContentsChanged (); + + return; + } + + List? rest = null; + var lastPosition = 0; + + if (_model.Count > 0 && line.Count > 0 && !_copyWithoutSelection) + { + // Keep a copy of the rest of the line + int restCount = line.Count - CurrentColumn; + rest = line.GetRange (CurrentColumn, restCount); + line.RemoveRange (CurrentColumn, restCount); + } + + // First line is inserted at the current location, the rest is appended + line.InsertRange (CurrentColumn, lines [0]); + + //model.AddLine (currentRow, lines [0]); + + List> addedLines = [new (line)]; + + for (var i = 1; i < lines.Count; i++) + { + _model.AddLine (CurrentRow + i, lines [i]); + + addedLines.Add ([.. lines [i]]); + } + + if (rest is { }) + { + List last = _model.GetLine (CurrentRow + lines.Count - 1); + lastPosition = last.Count; + last.InsertRange (last.Count, rest); + + addedLines.Last ().InsertRange (addedLines.Last ().Count, rest); + } + + _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added); + + // Now adjust column and row positions + CurrentRow += lines.Count - 1; + CurrentColumn = rest is { } ? lastPosition : lines [^1].Count; + Adjust (); + + _historyText.Add ( + [new (line)], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + OnContentsChanged (); + } + + private bool InsertText (Key a, Attribute? attribute = null) + { + //So that special keys like tab can be processed + if (_isReadOnly) + { + return true; + } + + SetWrapModel (); + + _historyText.Add ([new (GetCurrentLine ())], CursorPosition); + + if (IsSelecting) + { + ClearSelectedRegion (); + } + + if ((uint)a.KeyCode == '\n') + { + _model.AddLine (CurrentRow + 1, []); + CurrentRow++; + CurrentColumn = 0; + } + else if ((uint)a.KeyCode == '\r') + { + CurrentColumn = 0; + } + else + { + if (Used) + { + Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); + CurrentColumn++; + + if (CurrentColumn >= _leftColumn + Viewport.Width) + { + _leftColumn++; + SetNeedsDraw (); + } + } + else + { + Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); + CurrentColumn++; + } + } + + _historyText.Add ( + [new (GetCurrentLine ())], + CursorPosition, + TextEditingLineStatus.Replaced + ); + + UpdateWrapModel (); + OnContentsChanged (); + + return true; + } + + #region History Event Handlers + + private void HistoryText_ChangeText (object sender, HistoryTextItemEventArgs obj) + { + SetWrapModel (); + + if (obj is { }) + { + int startLine = obj.CursorPosition.Y; + + if (obj.RemovedOnAdded is { }) + { + int offset; + + if (obj.IsUndoing) + { + offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); + } + else + { + offset = obj.RemovedOnAdded.Lines.Count - 1; + } + + for (var i = 0; i < offset; i++) + { + if (Lines > obj.RemovedOnAdded.CursorPosition.Y) + { + _model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); + } + else + { + break; + } + } + } + + for (var i = 0; i < obj.Lines.Count; i++) + { + if (i == 0 || obj.LineStatus == TextEditingLineStatus.Original || obj.LineStatus == TextEditingLineStatus.Attribute) + { + _model.ReplaceLine (startLine, obj.Lines [i]); + } + else if (obj is { IsUndoing: true, LineStatus: TextEditingLineStatus.Removed } + or { IsUndoing: false, LineStatus: TextEditingLineStatus.Added }) + { + _model.AddLine (startLine, obj.Lines [i]); + } + else if (Lines > obj.CursorPosition.Y + 1) + { + _model.RemoveLine (obj.CursorPosition.Y + 1); + } + + startLine++; + } + + CursorPosition = obj.FinalCursorPosition; + } + + UpdateWrapModel (); + + Adjust (); + OnContentsChanged (); + } + + #endregion +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs new file mode 100644 index 000000000..6b64d784f --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Navigation.cs @@ -0,0 +1,599 @@ +namespace Terminal.Gui.Views; + +/// Navigation functionality - cursor movement and scrolling +public partial class TextView +{ + #region Public Navigation Methods + + /// Will scroll the to the last line and position the cursor there. + public void MoveEnd () + { + CurrentRow = _model.Count - 1; + List line = GetCurrentLine (); + CurrentColumn = line.Count; + TrackColumn (); + DoNeededAction (); + } + + /// Will scroll the to the first line and position the cursor there. + public void MoveHome () + { + CurrentRow = 0; + _topRow = 0; + CurrentColumn = 0; + _leftColumn = 0; + TrackColumn (); + DoNeededAction (); + } + + /// + /// Will scroll the to display the specified row at the top if is + /// true or will scroll the to display the specified column at the left if + /// is false. + /// + /// + /// Row that should be displayed at the top or Column that should be displayed at the left, if the value + /// is negative it will be reset to zero + /// + /// If true (default) the is a row, column otherwise. + public void ScrollTo (int idx, bool isRow = true) + { + if (idx < 0) + { + idx = 0; + } + + if (isRow) + { + _topRow = Math.Max (idx > _model.Count - 1 ? _model.Count - 1 : idx, 0); + + if (IsInitialized && Viewport.Y != _topRow) + { + Viewport = Viewport with { Y = _topRow }; + } + } + else if (!_wordWrap) + { + int maxlength = _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth); + _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0); + + if (IsInitialized && Viewport.X != _leftColumn) + { + Viewport = Viewport with { X = _leftColumn }; + } + } + + SetNeedsDraw (); + } + + #endregion + + #region Private Navigation Methods + + private void MoveBottomEnd () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveEnd (); + } + + private void MoveBottomEndExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveEnd (); + } + + private bool MoveDown () + { + if (CurrentRow + 1 < _model.Count) + { + if (_columnTrack == -1) + { + _columnTrack = CurrentColumn; + } + + CurrentRow++; + + if (CurrentRow >= _topRow + Viewport.Height) + { + _topRow++; + SetNeedsDraw (); + } + + TrackColumn (); + PositionCursor (); + } + else if (CurrentRow > Viewport.Height) + { + Adjust (); + } + else + { + return false; + } + + DoNeededAction (); + + return true; + } + + private void MoveEndOfLine () + { + List currentLine = GetCurrentLine (); + CurrentColumn = currentLine.Count; + DoNeededAction (); + } + + private bool MoveLeft () + { + if (CurrentColumn > 0) + { + CurrentColumn--; + } + else + { + if (CurrentRow > 0) + { + CurrentRow--; + + if (CurrentRow < _topRow) + { + _topRow--; + SetNeedsDraw (); + } + + List currentLine = GetCurrentLine (); + CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0); + } + else + { + return false; + } + } + + DoNeededAction (); + + return true; + } + + private void MovePageDown () + { + int nPageDnShift = Viewport.Height - 1; + + if (CurrentRow >= 0 && CurrentRow < _model.Count) + { + if (_columnTrack == -1) + { + _columnTrack = CurrentColumn; + } + + CurrentRow = CurrentRow + nPageDnShift > _model.Count + ? _model.Count > 0 ? _model.Count - 1 : 0 + : CurrentRow + nPageDnShift; + + if (_topRow < CurrentRow - nPageDnShift) + { + _topRow = CurrentRow >= _model.Count + ? CurrentRow - nPageDnShift + : _topRow + nPageDnShift; + SetNeedsDraw (); + } + + TrackColumn (); + PositionCursor (); + } + + DoNeededAction (); + } + + private void MovePageUp () + { + int nPageUpShift = Viewport.Height - 1; + + if (CurrentRow > 0) + { + if (_columnTrack == -1) + { + _columnTrack = CurrentColumn; + } + + CurrentRow = CurrentRow - nPageUpShift < 0 ? 0 : CurrentRow - nPageUpShift; + + if (CurrentRow < _topRow) + { + _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift; + SetNeedsDraw (); + } + + TrackColumn (); + PositionCursor (); + } + + DoNeededAction (); + } + + private bool MoveRight () + { + List currentLine = GetCurrentLine (); + + if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count) + { + CurrentColumn++; + } + else + { + if (CurrentRow + 1 < _model.Count) + { + CurrentRow++; + CurrentColumn = 0; + + if (CurrentRow >= _topRow + Viewport.Height) + { + _topRow++; + SetNeedsDraw (); + } + } + else + { + return false; + } + } + + DoNeededAction (); + + return true; + } + + private void MoveLeftStart () + { + if (_leftColumn > 0) + { + SetNeedsDraw (); + } + + CurrentColumn = 0; + _leftColumn = 0; + DoNeededAction (); + } + + private void MoveTopHome () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveHome (); + } + + private void MoveTopHomeExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveHome (); + } + + private bool MoveUp () + { + if (CurrentRow > 0) + { + if (_columnTrack == -1) + { + _columnTrack = CurrentColumn; + } + + CurrentRow--; + + if (CurrentRow < _topRow) + { + _topRow--; + SetNeedsDraw (); + } + + TrackColumn (); + PositionCursor (); + } + else + { + return false; + } + + DoNeededAction (); + + return true; + } + + private void MoveWordBackward () + { + (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); + + if (newPos.HasValue) + { + CurrentColumn = newPos.Value.col; + CurrentRow = newPos.Value.row; + } + + DoNeededAction (); + } + + private void MoveWordForward () + { + (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); + + if (newPos.HasValue) + { + CurrentColumn = newPos.Value.col; + CurrentRow = newPos.Value.row; + } + + DoNeededAction (); + } + + #endregion + + #region Process Navigation Methods + + private bool ProcessMoveDown () + { + ResetContinuousFindTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + return MoveDown (); + } + + private void ProcessMoveDownExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveDown (); + } + + private void ProcessMoveEndOfLine () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveEndOfLine (); + } + + private void ProcessMoveRightEndExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveEndOfLine (); + } + + private bool ProcessMoveLeft () + { + // if the user presses Left (without any control keys) and they are at the start of the text + if (CurrentColumn == 0 && CurrentRow == 0) + { + if (IsSelecting) + { + StopSelecting (); + + return true; + } + + // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward) + return false; + } + + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveLeft (); + + return true; + } + + private void ProcessMoveLeftExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveLeft (); + } + + private bool ProcessMoveRight () + { + // if the user presses Right (without any control keys) + // determine where the last cursor position in the text is + int lastRow = _model.Count - 1; + int lastCol = _model.GetLine (lastRow).Count; + + // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward) + if (CurrentColumn == lastCol && CurrentRow == lastRow) + { + // Unless they have text selected + if (IsSelecting) + { + // In which case clear + StopSelecting (); + + return true; + } + + return false; + } + + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveRight (); + + return true; + } + + private void ProcessMoveRightExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveRight (); + } + + private void ProcessMoveLeftStart () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveLeftStart (); + } + + private void ProcessMoveLeftStartExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveLeftStart (); + } + + private bool ProcessMoveUp () + { + ResetContinuousFindTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + return MoveUp (); + } + + private void ProcessMoveUpExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveUp (); + } + + private void ProcessMoveWordBackward () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveWordBackward (); + } + + private void ProcessMoveWordBackwardExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveWordBackward (); + } + + private void ProcessMoveWordForward () + { + ResetAllTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MoveWordForward (); + } + + private void ProcessMoveWordForwardExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveWordForward (); + } + + private void ProcessPageDown () + { + ResetColumnTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MovePageDown (); + } + + private void ProcessPageDownExtend () + { + ResetColumnTrack (); + StartSelecting (); + MovePageDown (); + } + + private void ProcessPageUp () + { + ResetColumnTrack (); + + if (_shiftSelecting && IsSelecting) + { + StopSelecting (); + } + + MovePageUp (); + } + + private void ProcessPageUpExtend () + { + ResetColumnTrack (); + StartSelecting (); + MovePageUp (); + } + + #endregion + + #region Column Tracking + + // Tries to snap the cursor to the tracking column + private void TrackColumn () + { + // Now track the column + List line = GetCurrentLine (); + + if (line.Count < _columnTrack) + { + CurrentColumn = line.Count; + } + else if (_columnTrack != -1) + { + CurrentColumn = _columnTrack; + } + else if (CurrentColumn > line.Count) + { + CurrentColumn = line.Count; + } + + Adjust (); + } + + #endregion +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs new file mode 100644 index 000000000..a6593371d --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Selection.cs @@ -0,0 +1,399 @@ +namespace Terminal.Gui.Views; + +public partial class TextView +{ + + /// Get or sets whether the user is currently selecting text. + public bool IsSelecting { get; set; } + + /// + /// Gets or sets whether the word navigation should select only the word itself without spaces around it or with the + /// spaces at right. + /// Default is false meaning that the spaces at right are included in the selection. + /// + public bool SelectWordOnlyOnDoubleClick { get; set; } + + /// Start row position of the selected text. + public int SelectionStartRow + { + get => _selectionStartRow; + set + { + _selectionStartRow = value < 0 ? 0 : + value > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value; + IsSelecting = true; + SetNeedsDraw (); + Adjust (); + } + } + + /// Start column position of the selected text. + public int SelectionStartColumn + { + get => _selectionStartColumn; + set + { + List line = _model.GetLine (_selectionStartRow); + + _selectionStartColumn = value < 0 ? 0 : + value > line.Count ? line.Count : value; + IsSelecting = true; + SetNeedsDraw (); + Adjust (); + } + } + + private void StartSelecting () + { + if (_shiftSelecting && IsSelecting) + { + return; + } + + _shiftSelecting = true; + IsSelecting = true; + _selectionStartColumn = CurrentColumn; + _selectionStartRow = CurrentRow; + } + + private void StopSelecting () + { + if (IsSelecting) + { + SetNeedsDraw (); + } + + _shiftSelecting = false; + IsSelecting = false; + _isButtonShift = false; + } + + + /// Length of the selected text. + public int SelectedLength => GetSelectedLength (); + + /// + /// Gets the selected text as + /// + /// List{List{Cell}} + /// + /// + public List> SelectedCellsList + { + get + { + GetRegion (out List> selectedCellsList); + + return selectedCellsList; + } + } + + /// The selected text. + public string SelectedText + { + get + { + if (!IsSelecting || (_model.Count == 1 && _model.GetLine (0).Count == 0)) + { + return string.Empty; + } + + return GetSelectedRegion (); + } + } + + + // Returns an encoded region start..end (top 32 bits are the row, low32 the column) + private void GetEncodedRegionBounds ( + out long start, + out long end, + int? startRow = null, + int? startCol = null, + int? cRow = null, + int? cCol = null + ) + { + long selection; + long point; + + if (startRow is null || startCol is null || cRow is null || cCol is null) + { + selection = ((long)(uint)_selectionStartRow << 32) | (uint)_selectionStartColumn; + point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; + } + else + { + selection = ((long)(uint)startRow << 32) | (uint)startCol; + point = ((long)(uint)cRow << 32) | (uint)cCol; + } + + if (selection > point) + { + start = point; + end = selection; + } + else + { + start = selection; + end = point; + } + } + + // + // Returns a string with the text in the selected + // region. + // + internal string GetRegion ( + out List> cellsList, + int? sRow = null, + int? sCol = null, + int? cRow = null, + int? cCol = null, + TextModel? model = null + ) + { + GetEncodedRegionBounds (out long start, out long end, sRow, sCol, cRow, cCol); + + cellsList = []; + + if (start == end) + { + return string.Empty; + } + + var startRow = (int)(start >> 32); + var maxRow = (int)(end >> 32); + var startCol = (int)(start & 0xffffffff); + var endCol = (int)(end & 0xffffffff); + List line = model is null ? _model.GetLine (startRow) : model.GetLine (startRow); + List cells; + + if (startRow == maxRow) + { + cells = line.GetRange (startCol, endCol - startCol); + cellsList.Add (cells); + + return StringFromCells (cells); + } + + cells = line.GetRange (startCol, line.Count - startCol); + cellsList.Add (cells); + string res = StringFromCells (cells); + + for (int row = startRow + 1; row < maxRow; row++) + { + cellsList.AddRange ([]); + cells = model == null ? _model.GetLine (row) : model.GetLine (row); + cellsList.Add (cells); + + res = res + + Environment.NewLine + + StringFromCells (cells); + } + + line = model is null ? _model.GetLine (maxRow) : model.GetLine (maxRow); + cellsList.AddRange ([]); + cells = line.GetRange (0, endCol); + cellsList.Add (cells); + res = res + Environment.NewLine + StringFromCells (cells); + + return res; + } + + private int GetSelectedLength () { return SelectedText.Length; } + + private string GetSelectedRegion () + { + int cRow = CurrentRow; + int cCol = CurrentColumn; + int startRow = _selectionStartRow; + int startCol = _selectionStartColumn; + TextModel model = _model; + + if (_wordWrap) + { + cRow = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow); + cCol = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); + startRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow); + startCol = _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn); + model = _wrapManager.Model; + } + + OnUnwrappedCursorPosition (cRow, cCol); + + return GetRegion (out _, startRow, startCol, cRow, cCol, model); + } + + + private string StringFromCells (List cells) + { + ArgumentNullException.ThrowIfNull (cells); + + var size = 0; + foreach (Cell cell in cells) + { + string t = cell.Grapheme; + size += Encoding.Unicode.GetByteCount (t); + } + + byte [] encoded = new byte [size]; + var offset = 0; + foreach (Cell cell in cells) + { + string t = cell.Grapheme; + int bytesWritten = Encoding.Unicode.GetBytes (t, 0, t.Length, encoded, offset); + offset += bytesWritten; + } + + // decode using the same encoding and the bytes actually written + return Encoding.Unicode.GetString (encoded, 0, offset); + } + + /// + public bool EnableForDesign () + { + Text = """ + TextView provides a fully featured multi-line text editor. + It supports word wrap and history for undo. + """; + + return true; + } + + + /// + protected override void Dispose (bool disposing) + { + if (disposing && ContextMenu is { }) + { + ContextMenu.Visible = false; + ContextMenu.Dispose (); + ContextMenu = null; + } + + base.Dispose (disposing); + } + + private void ClearRegion () + { + SetWrapModel (); + + long start, end; + long currentEncoded = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; + GetEncodedRegionBounds (out start, out end); + var startRow = (int)(start >> 32); + var maxrow = (int)(end >> 32); + var startCol = (int)(start & 0xffffffff); + var endCol = (int)(end & 0xffffffff); + List line = _model.GetLine (startRow); + + _historyText.Add (new () { new (line) }, new (startCol, startRow)); + + List> removedLines = new (); + + if (startRow == maxrow) + { + removedLines.Add (new (line)); + + line.RemoveRange (startCol, endCol - startCol); + CurrentColumn = startCol; + + if (_wordWrap) + { + SetNeedsDraw (); + } + else + { + //QUESTION: Is the below comment still relevant? + // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. + //SetNeedsDraw (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1)); + SetNeedsDraw (); + } + + _historyText.Add ( + new (removedLines), + CursorPosition, + TextEditingLineStatus.Removed + ); + + UpdateWrapModel (); + + return; + } + + removedLines.Add (new (line)); + + line.RemoveRange (startCol, line.Count - startCol); + List line2 = _model.GetLine (maxrow); + line.AddRange (line2.Skip (endCol)); + + for (int row = startRow + 1; row <= maxrow; row++) + { + removedLines.Add (new (_model.GetLine (startRow + 1))); + + _model.RemoveLine (startRow + 1); + } + + if (currentEncoded == end) + { + CurrentRow -= maxrow - startRow; + } + + CurrentColumn = startCol; + + _historyText.Add ( + new (removedLines), + CursorPosition, + TextEditingLineStatus.Removed + ); + + UpdateWrapModel (); + + SetNeedsDraw (); + } + + private void ClearSelectedRegion () + { + SetWrapModel (); + + if (!_isReadOnly) + { + ClearRegion (); + } + + UpdateWrapModel (); + IsSelecting = false; + DoNeededAction (); + } + + /// Select all text. + public void SelectAll () + { + if (_model.Count == 0) + { + return; + } + + StartSelecting (); + _selectionStartColumn = 0; + _selectionStartRow = 0; + CurrentColumn = _model.GetLine (_model.Count - 1).Count; + CurrentRow = _model.Count - 1; + SetNeedsDraw (); + } + + private void ProcessSelectAll () + { + ResetColumnTrack (); + SelectAll (); + } + + private bool PointInSelection (int col, int row) + { + long start, end; + GetEncodedRegionBounds (out start, out end); + long q = ((long)(uint)row << 32) | (uint)col; + + return q >= start && q <= end - 1; + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Utilities.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Utilities.cs new file mode 100644 index 000000000..3cd27a393 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Utilities.cs @@ -0,0 +1,175 @@ +namespace Terminal.Gui.Views; + +/// Utility and helper methods for TextView +public partial class TextView +{ + private void Adjust () + { + (int width, int height) offB = OffSetBackground (); + List line = GetCurrentLine (); + bool need = NeedsDraw || _wrapNeeded || !Used; + (int size, int length) tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth); + (int size, int length) dSize = TextModel.DisplaySize (line, _leftColumn, CurrentColumn, true, TabWidth); + + if (!_wordWrap && CurrentColumn < _leftColumn) + { + _leftColumn = CurrentColumn; + need = true; + } + else if (!_wordWrap + && (CurrentColumn - _leftColumn + 1 > Viewport.Width + offB.width || dSize.size + 1 >= Viewport.Width + offB.width)) + { + _leftColumn = TextModel.CalculateLeftColumn ( + line, + _leftColumn, + CurrentColumn, + Viewport.Width + offB.width, + TabWidth + ); + need = true; + } + else if ((_wordWrap && _leftColumn > 0) || (dSize.size < Viewport.Width + offB.width && tSize.size < Viewport.Width + offB.width)) + { + if (_leftColumn > 0) + { + _leftColumn = 0; + need = true; + } + } + + if (CurrentRow < _topRow) + { + _topRow = CurrentRow; + need = true; + } + else if (CurrentRow - _topRow >= Viewport.Height + offB.height) + { + _topRow = Math.Min (Math.Max (CurrentRow - Viewport.Height + 1, 0), CurrentRow); + need = true; + } + else if (_topRow > 0 && CurrentRow < _topRow) + { + _topRow = Math.Max (_topRow - 1, 0); + need = true; + } + + // Sync Viewport with the internal scroll position + if (IsInitialized && (_leftColumn != Viewport.X || _topRow != Viewport.Y)) + { + Viewport = new Rectangle (_leftColumn, _topRow, Viewport.Width, Viewport.Height); + } + + if (need) + { + if (_wrapNeeded) + { + WrapTextModel (); + _wrapNeeded = false; + } + + SetNeedsDraw (); + } + else + { + if (IsInitialized) + { + PositionCursor (); + } + } + + OnUnwrappedCursorPosition (); + } + + private void DoNeededAction () + { + if (!NeedsDraw && (IsSelecting || _wrapNeeded || !Used)) + { + SetNeedsDraw (); + } + + if (NeedsDraw) + { + Adjust (); + } + else + { + PositionCursor (); + OnUnwrappedCursorPosition (); + } + } + + private (int width, int height) OffSetBackground () + { + var w = 0; + var h = 0; + + if (SuperView?.Viewport.Right - Viewport.Right < 0) + { + w = SuperView!.Viewport.Right - Viewport.Right - 1; + } + + if (SuperView?.Viewport.Bottom - Viewport.Bottom < 0) + { + h = SuperView!.Viewport.Bottom - Viewport.Bottom - 1; + } + + return (w, h); + } + + /// + /// Updates the content size based on the text model dimensions. + /// + private void UpdateContentSize () + { + int contentHeight = Math.Max (_model.Count, 1); + + // For horizontal size: if word wrap is enabled, content width equals viewport width + // Otherwise, calculate the maximum line width (but only if we have a reasonable viewport) + int contentWidth; + + if (_wordWrap) + { + // Word wrap: content width follows viewport width + contentWidth = Math.Max (Viewport.Width, 1); + } + else + { + // No word wrap: calculate max line width + // Cache the current value to avoid recalculating on every call + contentWidth = Math.Max (_model.GetMaxVisibleLine (0, _model.Count, TabWidth), 1); + } + + SetContentSize (new Size (contentWidth, contentHeight)); + } + + private void ResetPosition () + { + _topRow = _leftColumn = CurrentRow = CurrentColumn = 0; + StopSelecting (); + } + + private void ResetAllTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + _lastWasKill = false; + _columnTrack = -1; + _continuousFind = false; + } + + private void ResetColumnTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + _lastWasKill = false; + _columnTrack = -1; + } + + private void ToggleSelecting () + { + ResetColumnTrack (); + IsSelecting = !IsSelecting; + _selectionStartColumn = CurrentColumn; + _selectionStartRow = CurrentRow; + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs new file mode 100644 index 000000000..99b26a8e4 --- /dev/null +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.WordWrap.cs @@ -0,0 +1,125 @@ +using System.Runtime.CompilerServices; + +namespace Terminal.Gui.Views; + +/// Word wrap functionality +public partial class TextView +{ + /// Invoke the event with the unwrapped . + public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null) + { + int? row = cRow ?? CurrentRow; + int? col = cCol ?? CurrentColumn; + + if (cRow is null && cCol is null && _wordWrap) + { + row = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow); + col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); + } + + UnwrappedCursorPosition?.Invoke (this, new (col.Value, row.Value)); + } + + /// Invoked with the unwrapped . + public event EventHandler? UnwrappedCursorPosition; + + private (int Row, int Col) GetUnwrappedPosition (int line, int col) + { + if (WordWrap) + { + return new ValueTuple ( + _wrapManager!.GetModelLineFromWrappedLines (line), + _wrapManager.GetModelColFromWrappedLines (line, col) + ); + } + + return new ValueTuple (line, col); + } + + /// Restore from original model. + private void SetWrapModel ([CallerMemberName] string? caller = null) + { + if (_currentCaller is { }) + { + return; + } + + if (_wordWrap) + { + _currentCaller = caller; + + CurrentColumn = _wrapManager!.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); + CurrentRow = _wrapManager.GetModelLineFromWrappedLines (CurrentRow); + + _selectionStartColumn = + _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn); + _selectionStartRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow); + _model = _wrapManager.Model; + } + } + + /// Update the original model. + private void UpdateWrapModel ([CallerMemberName] string? caller = null) + { + if (_currentCaller is { } && _currentCaller != caller) + { + return; + } + + if (_wordWrap) + { + _currentCaller = null; + + _wrapManager!.UpdateModel ( + _model, + out int nRow, + out int nCol, + out int nStartRow, + out int nStartCol, + CurrentRow, + CurrentColumn, + _selectionStartRow, + _selectionStartColumn, + true + ); + CurrentRow = nRow; + CurrentColumn = nCol; + _selectionStartRow = nStartRow; + _selectionStartColumn = nStartCol; + _wrapNeeded = true; + + SetNeedsDraw (); + } + + if (_currentCaller is { }) + { + throw new InvalidOperationException ( + $"WordWrap settings was changed after the {_currentCaller} call." + ); + } + } + + private void WrapTextModel () + { + if (_wordWrap && _wrapManager is { }) + { + _model = _wrapManager.WrapModel ( + Math.Max (Viewport.Width - (ReadOnly ? 0 : 1), 0), // For the cursor on the last column of a line + out int nRow, + out int nCol, + out int nStartRow, + out int nStartCol, + CurrentRow, + CurrentColumn, + _selectionStartRow, + _selectionStartColumn, + _tabWidth + ); + CurrentRow = nRow; + CurrentColumn = nCol; + _selectionStartRow = nStartRow; + _selectionStartColumn = nStartCol; + SetNeedsDraw (); + } + } +} diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs index 0864a6672..3b0625cb1 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs @@ -72,562 +72,8 @@ namespace Terminal.Gui.Views; /// /// /// -public class TextView : View, IDesignable +public partial class TextView : View, IDesignable { - private readonly HistoryText _historyText = new (); - private bool _allowsReturn = true; - private bool _allowsTab = true; - private bool _clickWithSelecting; - - // The column we are tracking, or -1 if we are not tracking any column - private int _columnTrack = -1; - private bool _continuousFind; - private bool _copyWithoutSelection; - private string? _currentCaller; - private CultureInfo? _currentCulture; - private bool _isButtonShift; - private bool _isButtonReleased; - private bool _isDrawing; - private bool _isReadOnly; - private bool _lastWasKill; - private int _leftColumn; - private TextModel _model = new (); - private bool _multiline = true; - private Dim? _savedHeight; - private int _selectionStartColumn, _selectionStartRow; - private bool _shiftSelecting; - private int _tabWidth = 4; - private int _topRow; - private bool _wordWrap; - private WordWrapManager? _wrapManager; - private bool _wrapNeeded; - - /// - /// Initializes a on the specified area, with dimensions controlled with the X, Y, Width - /// and Height properties. - /// - public TextView () - { - CanFocus = true; - CursorVisibility = CursorVisibility.Default; - Used = true; - - // By default, disable hotkeys (in case someone sets Title) - base.HotKeySpecifier = new ('\xffff'); - - _model.LinesLoaded += Model_LinesLoaded!; - _historyText.ChangeText += HistoryText_ChangeText!; - - Initialized += TextView_Initialized!; - - SuperViewChanged += TextView_SuperViewChanged!; - - SubViewsLaidOut += TextView_LayoutComplete; - - // Things this view knows how to do - - // Note - NewLine is only bound to Enter if Multiline is true - AddCommand (Command.NewLine, ctx => ProcessEnterKey (ctx)); - - AddCommand ( - Command.PageDown, - () => - { - ProcessPageDown (); - - return true; - } - ); - - AddCommand ( - Command.PageDownExtend, - () => - { - ProcessPageDownExtend (); - - return true; - } - ); - - AddCommand ( - Command.PageUp, - () => - { - ProcessPageUp (); - - return true; - } - ); - - AddCommand ( - Command.PageUpExtend, - () => - { - ProcessPageUpExtend (); - - return true; - } - ); - - AddCommand (Command.Down, () => ProcessMoveDown ()); - - AddCommand ( - Command.DownExtend, - () => - { - ProcessMoveDownExtend (); - - return true; - } - ); - - AddCommand (Command.Up, () => ProcessMoveUp ()); - - AddCommand ( - Command.UpExtend, - () => - { - ProcessMoveUpExtend (); - - return true; - } - ); - AddCommand (Command.Right, () => ProcessMoveRight ()); - - AddCommand ( - Command.RightExtend, - () => - { - ProcessMoveRightExtend (); - - return true; - } - ); - AddCommand (Command.Left, () => ProcessMoveLeft ()); - - AddCommand ( - Command.LeftExtend, - () => - { - ProcessMoveLeftExtend (); - - return true; - } - ); - - AddCommand ( - Command.DeleteCharLeft, - () => - { - ProcessDeleteCharLeft (); - - return true; - } - ); - - AddCommand ( - Command.LeftStart, - () => - { - ProcessMoveLeftStart (); - - return true; - } - ); - - AddCommand ( - Command.LeftStartExtend, - () => - { - ProcessMoveLeftStartExtend (); - - return true; - } - ); - - AddCommand ( - Command.DeleteCharRight, - () => - { - ProcessDeleteCharRight (); - - return true; - } - ); - - AddCommand ( - Command.RightEnd, - () => - { - ProcessMoveEndOfLine (); - - return true; - } - ); - - AddCommand ( - Command.RightEndExtend, - () => - { - ProcessMoveRightEndExtend (); - - return true; - } - ); - - AddCommand ( - Command.CutToEndLine, - () => - { - KillToEndOfLine (); - - return true; - } - ); - - AddCommand ( - Command.CutToStartLine, - () => - { - KillToLeftStart (); - - return true; - } - ); - - AddCommand ( - Command.Paste, - () => - { - ProcessPaste (); - - return true; - } - ); - - AddCommand ( - Command.ToggleExtend, - () => - { - ToggleSelecting (); - - return true; - } - ); - - AddCommand ( - Command.Copy, - () => - { - ProcessCopy (); - - return true; - } - ); - - AddCommand ( - Command.Cut, - () => - { - ProcessCut (); - - return true; - } - ); - - AddCommand ( - Command.WordLeft, - () => - { - ProcessMoveWordBackward (); - - return true; - } - ); - - AddCommand ( - Command.WordLeftExtend, - () => - { - ProcessMoveWordBackwardExtend (); - - return true; - } - ); - - AddCommand ( - Command.WordRight, - () => - { - ProcessMoveWordForward (); - - return true; - } - ); - - AddCommand ( - Command.WordRightExtend, - () => - { - ProcessMoveWordForwardExtend (); - - return true; - } - ); - - AddCommand ( - Command.KillWordForwards, - () => - { - ProcessKillWordForward (); - - return true; - } - ); - - AddCommand ( - Command.KillWordBackwards, - () => - { - ProcessKillWordBackward (); - - return true; - } - ); - - AddCommand ( - Command.End, - () => - { - MoveBottomEnd (); - - return true; - } - ); - - AddCommand ( - Command.EndExtend, - () => - { - MoveBottomEndExtend (); - - return true; - } - ); - - AddCommand ( - Command.Start, - () => - { - MoveTopHome (); - - return true; - } - ); - - AddCommand ( - Command.StartExtend, - () => - { - MoveTopHomeExtend (); - - return true; - } - ); - - AddCommand ( - Command.SelectAll, - () => - { - ProcessSelectAll (); - - return true; - } - ); - - AddCommand ( - Command.ToggleOverwrite, - () => - { - ProcessSetOverwrite (); - - return true; - } - ); - - AddCommand ( - Command.EnableOverwrite, - () => - { - SetOverwrite (true); - - return true; - } - ); - - AddCommand ( - Command.DisableOverwrite, - () => - { - SetOverwrite (false); - - return true; - } - ); - AddCommand (Command.Tab, () => ProcessTab ()); - AddCommand (Command.BackTab, () => ProcessBackTab ()); - - AddCommand ( - Command.Undo, - () => - { - Undo (); - - return true; - } - ); - - AddCommand ( - Command.Redo, - () => - { - Redo (); - - return true; - } - ); - - AddCommand ( - Command.DeleteAll, - () => - { - DeleteAll (); - - return true; - } - ); - - AddCommand ( - Command.Context, - () => - { - ShowContextMenu (null); - - return true; - } - ); - - AddCommand ( - Command.Open, - () => - { - PromptForColors (); - - return true; - }); - - // Default keybindings for this view - KeyBindings.Remove (Key.Space); - - KeyBindings.Remove (Key.Enter); - KeyBindings.Add (Key.Enter, Multiline ? Command.NewLine : Command.Accept); - - KeyBindings.Add (Key.PageDown, Command.PageDown); - KeyBindings.Add (Key.V.WithCtrl, Command.PageDown); - - KeyBindings.Add (Key.PageDown.WithShift, Command.PageDownExtend); - - KeyBindings.Add (Key.PageUp, Command.PageUp); - - KeyBindings.Add (Key.PageUp.WithShift, Command.PageUpExtend); - - KeyBindings.Add (Key.N.WithCtrl, Command.Down); - KeyBindings.Add (Key.CursorDown, Command.Down); - - KeyBindings.Add (Key.CursorDown.WithShift, Command.DownExtend); - - KeyBindings.Add (Key.P.WithCtrl, Command.Up); - KeyBindings.Add (Key.CursorUp, Command.Up); - - KeyBindings.Add (Key.CursorUp.WithShift, Command.UpExtend); - - KeyBindings.Add (Key.F.WithCtrl, Command.Right); - KeyBindings.Add (Key.CursorRight, Command.Right); - - KeyBindings.Add (Key.CursorRight.WithShift, Command.RightExtend); - - KeyBindings.Add (Key.B.WithCtrl, Command.Left); - KeyBindings.Add (Key.CursorLeft, Command.Left); - - KeyBindings.Add (Key.CursorLeft.WithShift, Command.LeftExtend); - - KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); - - KeyBindings.Add (Key.Home, Command.LeftStart); - - KeyBindings.Add (Key.Home.WithShift, Command.LeftStartExtend); - - KeyBindings.Add (Key.Delete, Command.DeleteCharRight); - KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); - - KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); - - KeyBindings.Add (Key.End.WithShift, Command.RightEndExtend); - - KeyBindings.Add (Key.K.WithCtrl, Command.CutToEndLine); // kill-to-end - - KeyBindings.Add (Key.Delete.WithCtrl.WithShift, Command.CutToEndLine); // kill-to-end - - KeyBindings.Add (Key.Backspace.WithCtrl.WithShift, Command.CutToStartLine); // kill-to-start - - KeyBindings.Add (Key.Y.WithCtrl, Command.Paste); // Control-y, yank - KeyBindings.Add (Key.Space.WithCtrl, Command.ToggleExtend); - - KeyBindings.Add (Key.C.WithCtrl, Command.Copy); - - KeyBindings.Add (Key.W.WithCtrl, Command.Cut); // Move to Unix? - KeyBindings.Add (Key.X.WithCtrl, Command.Cut); - - KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.WordLeft); - - KeyBindings.Add (Key.CursorLeft.WithCtrl.WithShift, Command.WordLeftExtend); - - KeyBindings.Add (Key.CursorRight.WithCtrl, Command.WordRight); - - KeyBindings.Add (Key.CursorRight.WithCtrl.WithShift, Command.WordRightExtend); - KeyBindings.Add (Key.Delete.WithCtrl, Command.KillWordForwards); // kill-word-forwards - KeyBindings.Add (Key.Backspace.WithCtrl, Command.KillWordBackwards); // kill-word-backwards - - KeyBindings.Add (Key.End.WithCtrl, Command.End); - KeyBindings.Add (Key.End.WithCtrl.WithShift, Command.EndExtend); - KeyBindings.Add (Key.Home.WithCtrl, Command.Start); - KeyBindings.Add (Key.Home.WithCtrl.WithShift, Command.StartExtend); - KeyBindings.Add (Key.A.WithCtrl, Command.SelectAll); - KeyBindings.Add (Key.InsertChar, Command.ToggleOverwrite); - KeyBindings.Add (Key.Tab, Command.Tab); - KeyBindings.Add (Key.Tab.WithShift, Command.BackTab); - - KeyBindings.Add (Key.Z.WithCtrl, Command.Undo); - KeyBindings.Add (Key.R.WithCtrl, Command.Redo); - - KeyBindings.Add (Key.G.WithCtrl, Command.DeleteAll); - KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll); - - KeyBindings.Add (Key.L.WithCtrl, Command.Open); - -#if UNIX_KEY_BINDINGS - KeyBindings.Add (Key.C.WithAlt, Command.Copy); - KeyBindings.Add (Key.B.WithAlt, Command.WordLeft); - KeyBindings.Add (Key.W.WithAlt, Command.Cut); - KeyBindings.Add (Key.V.WithAlt, Command.PageUp); - KeyBindings.Add (Key.F.WithAlt, Command.WordRight); - KeyBindings.Add (Key.K.WithAlt, Command.CutToStartLine); // kill-to-start -#endif - - _currentCulture = Thread.CurrentThread.CurrentUICulture; - } - // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts. /// /// Gets or sets whether pressing ENTER in a creates a new line of text @@ -772,7 +218,7 @@ public class TextView : View, IDesignable int clampedValue = Math.Max (Math.Min (value, Maxlength - 1), 0); _leftColumn = clampedValue; - + if (IsInitialized && Viewport.X != _leftColumn) { Viewport = Viewport with { X = _leftColumn }; @@ -851,72 +297,6 @@ public class TextView : View, IDesignable } } - /// Length of the selected text. - public int SelectedLength => GetSelectedLength (); - - /// - /// Gets the selected text as - /// - /// List{List{Cell}} - /// - /// - public List> SelectedCellsList - { - get - { - GetRegion (out List> selectedCellsList); - - return selectedCellsList; - } - } - - /// The selected text. - public string SelectedText - { - get - { - if (!IsSelecting || (_model.Count == 1 && _model.GetLine (0).Count == 0)) - { - return string.Empty; - } - - return GetSelectedRegion (); - } - } - - /// Get or sets whether the user is currently selecting text. - public bool IsSelecting { get; set; } - - /// Start column position of the selected text. - public int SelectionStartColumn - { - get => _selectionStartColumn; - set - { - List line = _model.GetLine (_selectionStartRow); - - _selectionStartColumn = value < 0 ? 0 : - value > line.Count ? line.Count : value; - IsSelecting = true; - SetNeedsDraw (); - Adjust (); - } - } - - /// Start row position of the selected text. - public int SelectionStartRow - { - get => _selectionStartRow; - set - { - _selectionStartRow = value < 0 ? 0 : - value > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value; - IsSelecting = true; - SetNeedsDraw (); - Adjust (); - } - } - /// Gets or sets a value indicating the number of whitespace when pressing the TAB key. public int TabWidth { @@ -976,7 +356,7 @@ public class TextView : View, IDesignable { int clampedValue = Math.Max (Math.Min (value, Lines - 1), 0); _topRow = clampedValue; - + if (IsInitialized && Viewport.Y != _topRow) { Viewport = Viewport with { Y = _topRow }; @@ -1036,12 +416,7 @@ public class TextView : View, IDesignable /// public bool UseSameRuneTypeForWords { get; set; } - /// - /// Gets or sets whether the word navigation should select only the word itself without spaces around it or with the - /// spaces at right. - /// Default is false meaning that the spaces at right are included in the selection. - /// - public bool SelectWordOnlyOnDoubleClick { get; set; } + /// Allows clearing the items updating the original text. public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); } @@ -1066,77 +441,6 @@ public class TextView : View, IDesignable /// public event EventHandler? ContentsChanged; - internal void ApplyCellsAttribute (Attribute attribute) - { - if (!ReadOnly && SelectedLength > 0) - { - int startRow = Math.Min (SelectionStartRow, CurrentRow); - int endRow = Math.Max (CurrentRow, SelectionStartRow); - int startCol = SelectionStartRow <= CurrentRow ? SelectionStartColumn : CurrentColumn; - int endCol = CurrentRow >= SelectionStartRow ? CurrentColumn : SelectionStartColumn; - List> selectedCellsOriginal = []; - List> selectedCellsChanged = []; - - for (int r = startRow; r <= endRow; r++) - { - List line = GetLine (r); - - selectedCellsOriginal.Add ([.. line]); - - for (int c = r == startRow ? startCol : 0; - c < (r == endRow ? endCol : line.Count); - c++) - { - Cell cell = line [c]; // Copy value to a new variable - cell.Attribute = attribute; // Modify the copy - line [c] = cell; // Assign the modified copy back - } - - selectedCellsChanged.Add ([.. GetLine (r)]); - } - - GetSelectedRegion (); - IsSelecting = false; - - _historyText.Add ( - [.. selectedCellsOriginal], - new Point (startCol, startRow) - ); - - _historyText.Add ( - [.. selectedCellsChanged], - new Point (startCol, startRow), - TextEditingLineStatus.Attribute - ); - } - } - - private Attribute? GetSelectedCellAttribute () - { - List line; - - if (SelectedLength > 0) - { - line = GetLine (SelectionStartRow); - - if (line [Math.Min (SelectionStartColumn, line.Count - 1)].Attribute is { } attributeSel) - { - return new (attributeSel); - } - - return GetAttributeForRole (VisualRole.Active); - } - - line = GetCurrentLine (); - - if (line [Math.Min (CurrentColumn, line.Count - 1)].Attribute is { } attribute) - { - return new (attribute); - } - - return GetAttributeForRole (VisualRole.Active); - } - /// /// Open a dialog to set the foreground and background colors. /// @@ -1160,244 +464,6 @@ public class TextView : View, IDesignable ApplyCellsAttribute (attribute); } - private string? _copiedText; - private List> _copiedCellsList = []; - - /// Copy the selected text to the clipboard contents. - public void Copy () - { - SetWrapModel (); - - if (IsSelecting) - { - _copiedText = GetRegion (out _copiedCellsList); - SetClipboard (_copiedText); - _copyWithoutSelection = false; - } - else - { - List currentLine = GetCurrentLine (); - _copiedCellsList.Add (currentLine); - _copiedText = Cell.ToString (currentLine); - SetClipboard (_copiedText); - _copyWithoutSelection = true; - } - - UpdateWrapModel (); - DoNeededAction (); - } - - /// Cut the selected text to the clipboard contents. - public void Cut () - { - SetWrapModel (); - _copiedText = GetRegion (out _copiedCellsList); - SetClipboard (_copiedText); - - if (!_isReadOnly) - { - ClearRegion (); - - _historyText.Add ( - [new (GetCurrentLine ())], - CursorPosition, - TextEditingLineStatus.Replaced - ); - } - - UpdateWrapModel (); - IsSelecting = false; - DoNeededAction (); - OnContentsChanged (); - } - - /// Deletes all text. - public void DeleteAll () - { - if (Lines == 0) - { - return; - } - - _selectionStartColumn = 0; - _selectionStartRow = 0; - MoveBottomEndExtend (); - DeleteCharLeft (); - SetNeedsDraw (); - } - - /// Deletes all the selected or a single character at left from the position of the cursor. - public void DeleteCharLeft () - { - if (_isReadOnly) - { - return; - } - - SetWrapModel (); - - if (IsSelecting) - { - _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition); - - ClearSelectedRegion (); - - List currentLine = GetCurrentLine (); - - _historyText.Add ( - new () { new (currentLine) }, - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - OnContentsChanged (); - - return; - } - - if (DeleteTextBackwards ()) - { - UpdateWrapModel (); - OnContentsChanged (); - - return; - } - - UpdateWrapModel (); - - DoNeededAction (); - OnContentsChanged (); - } - - /// Deletes all the selected or a single character at right from the position of the cursor. - public void DeleteCharRight () - { - if (_isReadOnly) - { - return; - } - - SetWrapModel (); - - if (IsSelecting) - { - _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition); - - ClearSelectedRegion (); - - List currentLine = GetCurrentLine (); - - _historyText.Add ( - new () { new (currentLine) }, - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - OnContentsChanged (); - - return; - } - - if (DeleteTextForwards ()) - { - UpdateWrapModel (); - OnContentsChanged (); - - return; - } - - UpdateWrapModel (); - - DoNeededAction (); - OnContentsChanged (); - } - - /// Invoked when the normal color is drawn. - public event EventHandler? DrawNormalColor; - - /// Invoked when the ready only color is drawn. - public event EventHandler? DrawReadOnlyColor; - - /// Invoked when the selection color is drawn. - public event EventHandler? DrawSelectionColor; - - /// - /// Invoked when the used color is drawn. The Used Color is used to indicate if the - /// was pressed and enabled. - /// - public event EventHandler? DrawUsedColor; - - /// Find the next text based on the match case with the option to replace it. - /// The text to find. - /// trueIf all the text was forward searched.falseotherwise. - /// The match case setting. - /// The match whole word setting. - /// The text to replace. - /// trueIf is replacing.falseotherwise. - /// trueIf the text was found.falseotherwise. - public bool FindNextText ( - string textToFind, - out bool gaveFullTurn, - bool matchCase = false, - bool matchWholeWord = false, - string? textToReplace = null, - bool replace = false - ) - { - if (_model.Count == 0) - { - gaveFullTurn = false; - - return false; - } - - SetWrapModel (); - ResetContinuousFind (); - - (Point current, bool found) foundPos = - _model.FindNextText (textToFind, out gaveFullTurn, matchCase, matchWholeWord); - - return SetFoundText (textToFind, foundPos, textToReplace, replace); - } - - /// Find the previous text based on the match case with the option to replace it. - /// The text to find. - /// trueIf all the text was backward searched.falseotherwise. - /// The match case setting. - /// The match whole word setting. - /// The text to replace. - /// trueIf the text was found.falseotherwise. - /// trueIf the text was found.falseotherwise. - public bool FindPreviousText ( - string textToFind, - out bool gaveFullTurn, - bool matchCase = false, - bool matchWholeWord = false, - string? textToReplace = null, - bool replace = false - ) - { - if (_model.Count == 0) - { - gaveFullTurn = false; - - return false; - } - - SetWrapModel (); - ResetContinuousFind (); - - (Point current, bool found) foundPos = - _model.FindPreviousText (textToFind, out gaveFullTurn, matchCase, matchWholeWord); - - return SetFoundText (textToFind, foundPos, textToReplace, replace); - } - - /// Reset the flag to stop continuous find. - public void FindTextChanged () { _continuousFind = false; } - /// Gets all lines of characters. /// public List> GetAllLines () { return _model.GetAllLines (); } @@ -1414,54 +480,6 @@ public class TextView : View, IDesignable /// public List GetLine (int line) { return _model.GetLine (line); } - /// - protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute) - { - if (role == VisualRole.Normal) - { - currentAttribute = GetAttributeForRole (VisualRole.Editable); - - return true; - } - - return base.OnGettingAttributeForRole (role, ref currentAttribute); - } - - /// - /// Inserts the given text at the current cursor position exactly as if the user had just - /// typed it - /// - /// Text to add - public void InsertText (string toAdd) - { - foreach (char ch in toAdd) - { - Key key; - - try - { - key = new (ch); - } - catch (Exception) - { - throw new ArgumentException ( - $"Cannot insert character '{ch}' because it does not map to a Key" - ); - } - - InsertText (key); - - if (NeedsDraw) - { - Adjust (); - } - else - { - PositionCursor (); - } - } - } - /// Loads the contents of the file into the . /// true, if file was loaded, false otherwise. /// Path to the file to load. @@ -1768,27 +786,6 @@ public class TextView : View, IDesignable return true; } - /// Will scroll the to the last line and position the cursor there. - public void MoveEnd () - { - CurrentRow = _model.Count - 1; - List line = GetCurrentLine (); - CurrentColumn = line.Count; - TrackColumn (); - DoNeededAction (); - } - - /// Will scroll the to the first line and position the cursor there. - public void MoveHome () - { - CurrentRow = 0; - _topRow = 0; - CurrentColumn = 0; - _leftColumn = 0; - TrackColumn (); - DoNeededAction (); - } - /// /// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises the /// event. @@ -1799,7 +796,7 @@ public class TextView : View, IDesignable ProcessInheritsPreviousScheme (CurrentRow, CurrentColumn); ProcessAutocomplete (); - + // Update content size when content changes if (IsInitialized) { @@ -1807,104 +804,6 @@ public class TextView : View, IDesignable } } - /// - protected override bool OnDrawingContent () - { - _isDrawing = true; - - SetAttributeForRole (Enabled ? VisualRole.Editable : VisualRole.Disabled); - - (int width, int height) offB = OffSetBackground (); - int right = Viewport.Width + offB.width; - int bottom = Viewport.Height + offB.height; - var row = 0; - - for (int idxRow = _topRow; idxRow < _model.Count; idxRow++) - { - List line = _model.GetLine (idxRow); - int lineRuneCount = line.Count; - var col = 0; - - Move (0, row); - - for (int idxCol = _leftColumn; idxCol < lineRuneCount; idxCol++) - { - string text = idxCol >= lineRuneCount ? " " : line [idxCol].Grapheme; - int cols = text.GetColumns (false); - - if (idxCol < line.Count && IsSelecting && PointInSelection (idxCol, idxRow)) - { - OnDrawSelectionColor (line, idxCol, idxRow); - } - else if (idxCol == CurrentColumn && idxRow == CurrentRow && !IsSelecting && !Used && HasFocus && idxCol < lineRuneCount) - { - OnDrawUsedColor (line, idxCol, idxRow); - } - else if (ReadOnly) - { - OnDrawReadOnlyColor (line, idxCol, idxRow); - } - else - { - OnDrawNormalColor (line, idxCol, idxRow); - } - - if (text == "\t") - { - cols += TabWidth + 1; - - if (col + cols > right) - { - cols = right - col; - } - - for (var i = 0; i < cols; i++) - { - if (col + i < right) - { - AddRune (col + i, row, (Rune)' '); - } - } - } - else - { - AddStr (col, row, text); - - // Ensures that cols less than 0 to be 1 because it will be converted to a printable rune - cols = Math.Max (cols, 1); - } - - if (!TextModel.SetCol (ref col, Viewport.Right, cols)) - { - break; - } - - if (idxCol + 1 < lineRuneCount && col + line [idxCol + 1].Grapheme.GetColumns () > right) - { - break; - } - } - - if (col < right) - { - SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable); - ClearRegion (col, row, right, row + 1); - } - - row++; - } - - if (row < bottom) - { - SetAttributeForRole (ReadOnly ? VisualRole.ReadOnly : VisualRole.Editable); - ClearRegion (Viewport.Left, row, right, bottom); - } - - _isDrawing = false; - - return false; - } - /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { @@ -1964,86 +863,6 @@ public class TextView : View, IDesignable return false; } - /// Invoke the event with the unwrapped . - public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null) - { - int? row = cRow ?? CurrentRow; - int? col = cCol ?? CurrentColumn; - - if (cRow is null && cCol is null && _wordWrap) - { - row = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow); - col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); - } - - UnwrappedCursorPosition?.Invoke (this, new (col.Value, row.Value)); - } - - /// Paste the clipboard contents into the current selected position. - public void Paste () - { - if (_isReadOnly) - { - return; - } - - SetWrapModel (); - string? contents = Clipboard.Contents; - - if (_copyWithoutSelection && contents!.FirstOrDefault (x => x is '\n' or '\r') == 0) - { - List runeList = contents is null ? [] : Cell.ToCellList (contents); - List currentLine = GetCurrentLine (); - - _historyText.Add ([new (currentLine)], CursorPosition); - - List> addedLine = [new (currentLine), runeList]; - - _historyText.Add ( - [.. addedLine], - CursorPosition, - TextEditingLineStatus.Added - ); - - _model.AddLine (CurrentRow, runeList); - CurrentRow++; - - _historyText.Add ( - [new (GetCurrentLine ())], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - SetNeedsDraw (); - OnContentsChanged (); - } - else - { - if (IsSelecting) - { - ClearRegion (); - } - - _copyWithoutSelection = false; - InsertAllText (contents!, true); - - if (IsSelecting) - { - _historyText.ReplaceLast ( - [new (GetCurrentLine ())], - CursorPosition, - TextEditingLineStatus.Original - ); - } - - SetNeedsDraw (); - } - - UpdateWrapModel (); - IsSelecting = false; - DoNeededAction (); - } - /// Positions the cursor on the current row and column public override Point? PositionCursor () { @@ -2120,90 +939,6 @@ public class TextView : View, IDesignable _historyText.Redo (); } - /// Replaces all the text based on the match case. - /// The text to find. - /// The match case setting. - /// The match whole word setting. - /// The text to replace. - /// trueIf the text was found.falseotherwise. - public bool ReplaceAllText ( - string textToFind, - bool matchCase = false, - bool matchWholeWord = false, - string? textToReplace = null - ) - { - if (_isReadOnly || _model.Count == 0) - { - return false; - } - - SetWrapModel (); - ResetContinuousFind (); - - (Point current, bool found) foundPos = - _model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace); - - return SetFoundText (textToFind, foundPos, textToReplace, false, true); - } - - /// - /// Will scroll the to display the specified row at the top if is - /// true or will scroll the to display the specified column at the left if - /// is false. - /// - /// - /// Row that should be displayed at the top or Column that should be displayed at the left, if the value - /// is negative it will be reset to zero - /// - /// If true (default) the is a row, column otherwise. - public void ScrollTo (int idx, bool isRow = true) - { - if (idx < 0) - { - idx = 0; - } - - if (isRow) - { - _topRow = Math.Max (idx > _model.Count - 1 ? _model.Count - 1 : idx, 0); - - if (IsInitialized && Viewport.Y != _topRow) - { - Viewport = Viewport with { Y = _topRow }; - } - } - else if (!_wordWrap) - { - int maxlength = - _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth); - _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0); - - if (IsInitialized && Viewport.X != _leftColumn) - { - Viewport = Viewport with { X = _leftColumn }; - } - } - - SetNeedsDraw (); - } - - /// Select all text. - public void SelectAll () - { - if (_model.Count == 0) - { - return; - } - - StartSelecting (); - _selectionStartColumn = 0; - _selectionStartRow = 0; - CurrentColumn = _model.GetLine (_model.Count - 1).Count; - CurrentRow = _model.Count - 1; - SetNeedsDraw (); - } - ///// Raised when the property of the changes. ///// ///// The property of only changes when it is explicitly set, not as the @@ -2222,212 +957,6 @@ public class TextView : View, IDesignable _historyText.Undo (); } - /// Invoked with the unwrapped . - public event EventHandler? UnwrappedCursorPosition; - - /// - /// Sets the to an appropriate color for rendering the given - /// of the current . Override to provide custom coloring by calling - /// Defaults to . - /// - /// The line. - /// The col index. - /// The row index. - protected virtual void OnDrawNormalColor (List line, int idxCol, int idxRow) - { - (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); - var ev = new CellEventArgs (line, idxCol, unwrappedPos); - DrawNormalColor?.Invoke (this, ev); - - if (line [idxCol].Attribute is { }) - { - Attribute? attribute = line [idxCol].Attribute; - SetAttribute ((Attribute)attribute!); - } - else - { - SetAttribute (GetAttributeForRole (VisualRole.Normal)); - } - } - - /// - /// Sets the to an appropriate color for rendering the given - /// of the current . Override to provide custom coloring by calling - /// Defaults to . - /// - /// The line. - /// The col index. - /// /// - /// The row index. - protected virtual void OnDrawReadOnlyColor (List line, int idxCol, int idxRow) - { - (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); - var ev = new CellEventArgs (line, idxCol, unwrappedPos); - DrawReadOnlyColor?.Invoke (this, ev); - - Attribute? cellAttribute = line [idxCol].Attribute is { } ? line [idxCol].Attribute : GetAttributeForRole (VisualRole.ReadOnly); - - if (cellAttribute!.Value.Foreground == cellAttribute.Value.Background) - { - SetAttribute (new (cellAttribute.Value.Foreground, cellAttribute.Value.Background, cellAttribute.Value.Style)); - } - else - { - SetAttributeForRole (VisualRole.ReadOnly); - } - } - - /// - /// Sets the to an appropriate color for rendering the given - /// of the current . Override to provide custom coloring by calling - /// Defaults to . - /// - /// The line. - /// The col index. - /// /// - /// The row index. - protected virtual void OnDrawSelectionColor (List line, int idxCol, int idxRow) - { - (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); - var ev = new CellEventArgs (line, idxCol, unwrappedPos); - DrawSelectionColor?.Invoke (this, ev); - - if (line [idxCol].Attribute is { }) - { - Attribute? attribute = line [idxCol].Attribute; - Attribute? active = GetAttributeForRole (VisualRole.Active); - SetAttribute (new (active!.Value.Foreground, active.Value.Background, attribute!.Value.Style)); - } - else - { - SetAttributeForRole (VisualRole.Active); - } - } - - /// - /// Sets the to an appropriate color for rendering the given - /// of the current . Override to provide custom coloring by calling - /// Defaults to . - /// - /// The line. - /// The col index. - /// /// - /// The row index. - protected virtual void OnDrawUsedColor (List line, int idxCol, int idxRow) - { - (int Row, int Col) unwrappedPos = GetUnwrappedPosition (idxRow, idxCol); - var ev = new CellEventArgs (line, idxCol, unwrappedPos); - DrawUsedColor?.Invoke (this, ev); - - if (line [idxCol].Attribute is { }) - { - Attribute? attribute = line [idxCol].Attribute; - SetValidUsedColor (attribute!); - } - else - { - SetValidUsedColor (GetAttributeForRole (VisualRole.Focus)); - } - } - - private void Adjust () - { - (int width, int height) offB = OffSetBackground (); - List line = GetCurrentLine (); - bool need = NeedsDraw || _wrapNeeded || !Used; - (int size, int length) tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth); - (int size, int length) dSize = TextModel.DisplaySize (line, _leftColumn, CurrentColumn, true, TabWidth); - - if (!_wordWrap && CurrentColumn < _leftColumn) - { - _leftColumn = CurrentColumn; - need = true; - } - else if (!_wordWrap - && (CurrentColumn - _leftColumn + 1 > Viewport.Width + offB.width || dSize.size + 1 >= Viewport.Width + offB.width)) - { - _leftColumn = TextModel.CalculateLeftColumn ( - line, - _leftColumn, - CurrentColumn, - Viewport.Width + offB.width, - TabWidth - ); - need = true; - } - else if ((_wordWrap && _leftColumn > 0) || (dSize.size < Viewport.Width + offB.width && tSize.size < Viewport.Width + offB.width)) - { - if (_leftColumn > 0) - { - _leftColumn = 0; - need = true; - } - } - - if (CurrentRow < _topRow) - { - _topRow = CurrentRow; - need = true; - } - else if (CurrentRow - _topRow >= Viewport.Height + offB.height) - { - _topRow = Math.Min (Math.Max (CurrentRow - Viewport.Height + 1, 0), CurrentRow); - need = true; - } - else if (_topRow > 0 && CurrentRow < _topRow) - { - _topRow = Math.Max (_topRow - 1, 0); - need = true; - } - - // Sync Viewport with the internal scroll position - if (IsInitialized && (_leftColumn != Viewport.X || _topRow != Viewport.Y)) - { - Viewport = new Rectangle (_leftColumn, _topRow, Viewport.Width, Viewport.Height); - } - - if (need) - { - if (_wrapNeeded) - { - WrapTextModel (); - _wrapNeeded = false; - } - - SetNeedsDraw (); - } - else - { - if (IsInitialized) - { - PositionCursor (); - } - } - - OnUnwrappedCursorPosition (); - } - - private void AppendClipboard (string text) { Clipboard.Contents += text; } - - private PopoverMenu CreateContextMenu () - { - PopoverMenu menu = new ( - new List - { - new MenuItem (this, Command.SelectAll, Strings.ctxSelectAll), - new MenuItem (this, Command.DeleteAll, Strings.ctxDeleteAll), - new MenuItem (this, Command.Copy, Strings.ctxCopy), - new MenuItem (this, Command.Cut, Strings.ctxCut), - new MenuItem (this, Command.Paste, Strings.ctxPaste), - new MenuItem (this, Command.Undo, Strings.ctxUndo), - new MenuItem (this, Command.Redo, Strings.ctxRedo) - }); - - menu.KeyChanged += ContextMenu_KeyChanged; - - return menu; - } - private void ClearRegion (int left, int top, int right, int bottom) { for (int row = top; row < bottom; row++) @@ -2441,327 +970,6 @@ public class TextView : View, IDesignable } } - // - // Clears the contents of the selected region - // - private void ClearRegion () - { - SetWrapModel (); - - long start, end; - long currentEncoded = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; - GetEncodedRegionBounds (out start, out end); - var startRow = (int)(start >> 32); - var maxrow = (int)(end >> 32); - var startCol = (int)(start & 0xffffffff); - var endCol = (int)(end & 0xffffffff); - List line = _model.GetLine (startRow); - - _historyText.Add (new () { new (line) }, new (startCol, startRow)); - - List> removedLines = new (); - - if (startRow == maxrow) - { - removedLines.Add (new (line)); - - line.RemoveRange (startCol, endCol - startCol); - CurrentColumn = startCol; - - if (_wordWrap) - { - SetNeedsDraw (); - } - else - { - //QUESTION: Is the below comment still relevant? - // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDraw (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1)); - SetNeedsDraw (); - } - - _historyText.Add ( - new (removedLines), - CursorPosition, - TextEditingLineStatus.Removed - ); - - UpdateWrapModel (); - - return; - } - - removedLines.Add (new (line)); - - line.RemoveRange (startCol, line.Count - startCol); - List line2 = _model.GetLine (maxrow); - line.AddRange (line2.Skip (endCol)); - - for (int row = startRow + 1; row <= maxrow; row++) - { - removedLines.Add (new (_model.GetLine (startRow + 1))); - - _model.RemoveLine (startRow + 1); - } - - if (currentEncoded == end) - { - CurrentRow -= maxrow - startRow; - } - - CurrentColumn = startCol; - - _historyText.Add ( - new (removedLines), - CursorPosition, - TextEditingLineStatus.Removed - ); - - UpdateWrapModel (); - - SetNeedsDraw (); - } - - private void ClearSelectedRegion () - { - SetWrapModel (); - - if (!_isReadOnly) - { - ClearRegion (); - } - - UpdateWrapModel (); - IsSelecting = false; - DoNeededAction (); - } - - private void ContextMenu_KeyChanged (object? sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } - - private bool DeleteTextBackwards () - { - SetWrapModel (); - - if (CurrentColumn > 0) - { - // Delete backwards - List currentLine = GetCurrentLine (); - - _historyText.Add (new () { new (currentLine) }, CursorPosition); - - currentLine.RemoveAt (CurrentColumn - 1); - - if (_wordWrap) - { - _wrapNeeded = true; - } - - CurrentColumn--; - - _historyText.Add ( - new () { new (currentLine) }, - CursorPosition, - TextEditingLineStatus.Replaced - ); - - if (CurrentColumn < _leftColumn) - { - _leftColumn--; - SetNeedsDraw (); - } - else - { - // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDraw (new (0, currentRow - topRow, 1, Viewport.Width)); - SetNeedsDraw (); - } - } - else - { - // Merges the current line with the previous one. - if (CurrentRow == 0) - { - return true; - } - - int prowIdx = CurrentRow - 1; - List prevRow = _model.GetLine (prowIdx); - - _historyText.Add (new () { new (prevRow) }, CursorPosition); - - List> removedLines = new () { new (prevRow) }; - - removedLines.Add (new (GetCurrentLine ())); - - _historyText.Add ( - removedLines, - new (CurrentColumn, prowIdx), - TextEditingLineStatus.Removed - ); - - int prevCount = prevRow.Count; - _model.GetLine (prowIdx).AddRange (GetCurrentLine ()); - _model.RemoveLine (CurrentRow); - - if (_wordWrap) - { - _wrapNeeded = true; - } - - CurrentRow--; - - _historyText.Add ( - new () { GetCurrentLine () }, - new (CurrentColumn, prowIdx), - TextEditingLineStatus.Replaced - ); - - CurrentColumn = prevCount; - SetNeedsDraw (); - } - - UpdateWrapModel (); - - return false; - } - - private bool DeleteTextForwards () - { - SetWrapModel (); - - List currentLine = GetCurrentLine (); - - if (CurrentColumn == currentLine.Count) - { - if (CurrentRow + 1 == _model.Count) - { - UpdateWrapModel (); - - return true; - } - - _historyText.Add (new () { new (currentLine) }, CursorPosition); - - List> removedLines = new () { new (currentLine) }; - - List nextLine = _model.GetLine (CurrentRow + 1); - - removedLines.Add (new (nextLine)); - - _historyText.Add (removedLines, CursorPosition, TextEditingLineStatus.Removed); - - currentLine.AddRange (nextLine); - _model.RemoveLine (CurrentRow + 1); - - _historyText.Add ( - new () { new (currentLine) }, - CursorPosition, - TextEditingLineStatus.Replaced - ); - - if (_wordWrap) - { - _wrapNeeded = true; - } - - DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1)); - } - else - { - _historyText.Add ([ [.. currentLine]], CursorPosition); - - currentLine.RemoveAt (CurrentColumn); - - _historyText.Add ( - [ [.. currentLine]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - if (_wordWrap) - { - _wrapNeeded = true; - } - - DoSetNeedsDraw ( - new ( - CurrentColumn - _leftColumn, - CurrentRow - _topRow, - Viewport.Width, - Math.Max (CurrentRow - _topRow + 1, 0) - ) - ); - } - - UpdateWrapModel (); - - return false; - } - - private void DoNeededAction () - { - if (!NeedsDraw && (IsSelecting || _wrapNeeded || !Used)) - { - SetNeedsDraw (); - } - - if (NeedsDraw) - { - Adjust (); - } - else - { - PositionCursor (); - OnUnwrappedCursorPosition (); - } - } - - private void DoSetNeedsDraw (Rectangle rect) - { - if (_wrapNeeded) - { - SetNeedsDraw (); - } - else - { - // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDraw (rect); - SetNeedsDraw (); - } - } - - private IEnumerable<(int col, int row, Cell rune)> ForwardIterator (int col, int row) - { - if (col < 0 || row < 0) - { - yield break; - } - - if (row >= _model.Count) - { - yield break; - } - - List line = GetCurrentLine (); - - if (col >= line.Count) - { - yield break; - } - - while (row < _model.Count) - { - for (int c = col; c < line.Count; c++) - { - yield return (c, row, line [c]); - } - - col = 0; - row++; - line = GetCurrentLine (); - } - } - private void GenerateSuggestions () { List currentLine = GetCurrentLine (); @@ -2780,1065 +988,6 @@ public class TextView : View, IDesignable ); } - // Returns an encoded region start..end (top 32 bits are the row, low32 the column) - private void GetEncodedRegionBounds ( - out long start, - out long end, - int? startRow = null, - int? startCol = null, - int? cRow = null, - int? cCol = null - ) - { - long selection; - long point; - - if (startRow is null || startCol is null || cRow is null || cCol is null) - { - selection = ((long)(uint)_selectionStartRow << 32) | (uint)_selectionStartColumn; - point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; - } - else - { - selection = ((long)(uint)startRow << 32) | (uint)startCol; - point = ((long)(uint)cRow << 32) | (uint)cCol; - } - - if (selection > point) - { - start = point; - end = selection; - } - else - { - start = selection; - end = point; - } - } - - // - // Returns a string with the text in the selected - // region. - // - internal string GetRegion ( - out List> cellsList, - int? sRow = null, - int? sCol = null, - int? cRow = null, - int? cCol = null, - TextModel? model = null - ) - { - GetEncodedRegionBounds (out long start, out long end, sRow, sCol, cRow, cCol); - - cellsList = []; - - if (start == end) - { - return string.Empty; - } - - var startRow = (int)(start >> 32); - var maxRow = (int)(end >> 32); - var startCol = (int)(start & 0xffffffff); - var endCol = (int)(end & 0xffffffff); - List line = model is null ? _model.GetLine (startRow) : model.GetLine (startRow); - List cells; - - if (startRow == maxRow) - { - cells = line.GetRange (startCol, endCol - startCol); - cellsList.Add (cells); - - return StringFromCells (cells); - } - - cells = line.GetRange (startCol, line.Count - startCol); - cellsList.Add (cells); - string res = StringFromCells (cells); - - for (int row = startRow + 1; row < maxRow; row++) - { - cellsList.AddRange ([]); - cells = model == null ? _model.GetLine (row) : model.GetLine (row); - cellsList.Add (cells); - - res = res - + Environment.NewLine - + StringFromCells (cells); - } - - line = model is null ? _model.GetLine (maxRow) : model.GetLine (maxRow); - cellsList.AddRange ([]); - cells = line.GetRange (0, endCol); - cellsList.Add (cells); - res = res + Environment.NewLine + StringFromCells (cells); - - return res; - } - - private int GetSelectedLength () { return SelectedText.Length; } - - private string GetSelectedRegion () - { - int cRow = CurrentRow; - int cCol = CurrentColumn; - int startRow = _selectionStartRow; - int startCol = _selectionStartColumn; - TextModel model = _model; - - if (_wordWrap) - { - cRow = _wrapManager!.GetModelLineFromWrappedLines (CurrentRow); - cCol = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); - startRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow); - startCol = _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn); - model = _wrapManager.Model; - } - - OnUnwrappedCursorPosition (cRow, cCol); - - return GetRegion (out _, startRow, startCol, cRow, cCol, model); - } - - private (int Row, int Col) GetUnwrappedPosition (int line, int col) - { - if (WordWrap) - { - return new ValueTuple ( - _wrapManager!.GetModelLineFromWrappedLines (line), - _wrapManager.GetModelColFromWrappedLines (line, col) - ); - } - - return new ValueTuple (line, col); - } - - private void HistoryText_ChangeText (object sender, HistoryTextItemEventArgs obj) - { - SetWrapModel (); - - if (obj is { }) - { - int startLine = obj.CursorPosition.Y; - - if (obj.RemovedOnAdded is { }) - { - int offset; - - if (obj.IsUndoing) - { - offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1); - } - else - { - offset = obj.RemovedOnAdded.Lines.Count - 1; - } - - for (var i = 0; i < offset; i++) - { - if (Lines > obj.RemovedOnAdded.CursorPosition.Y) - { - _model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y); - } - else - { - break; - } - } - } - - for (var i = 0; i < obj.Lines.Count; i++) - { - if (i == 0 || obj.LineStatus == TextEditingLineStatus.Original || obj.LineStatus == TextEditingLineStatus.Attribute) - { - _model.ReplaceLine (startLine, obj.Lines [i]); - } - else if (obj is { IsUndoing: true, LineStatus: TextEditingLineStatus.Removed } - or { IsUndoing: false, LineStatus: TextEditingLineStatus.Added }) - { - _model.AddLine (startLine, obj.Lines [i]); - } - else if (Lines > obj.CursorPosition.Y + 1) - { - _model.RemoveLine (obj.CursorPosition.Y + 1); - } - - startLine++; - } - - CursorPosition = obj.FinalCursorPosition; - } - - UpdateWrapModel (); - - Adjust (); - OnContentsChanged (); - } - - private void Insert (Cell cell) - { - List line = GetCurrentLine (); - - if (Used) - { - line.Insert (Math.Min (CurrentColumn, line.Count), cell); - } - else - { - if (CurrentColumn < line.Count) - { - line.RemoveAt (CurrentColumn); - } - - line.Insert (Math.Min (CurrentColumn, line.Count), cell); - } - - int prow = CurrentRow - _topRow; - - if (!_wrapNeeded) - { - // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDraw (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0))); - SetNeedsDraw (); - } - } - - private void InsertAllText (string text, bool fromClipboard = false) - { - if (string.IsNullOrEmpty (text)) - { - return; - } - - List> lines; - - if (fromClipboard && text == _copiedText) - { - lines = _copiedCellsList; - } - else - { - // Get selected attribute - Attribute? attribute = GetSelectedAttribute (CurrentRow, CurrentColumn); - lines = Cell.StringToLinesOfCells (text, attribute); - } - - if (lines.Count == 0) - { - return; - } - - SetWrapModel (); - - List line = GetCurrentLine (); - - _historyText.Add ([new (line)], CursorPosition); - - // Optimize single line - if (lines.Count == 1) - { - line.InsertRange (CurrentColumn, lines [0]); - CurrentColumn += lines [0].Count; - - _historyText.Add ( - [new (line)], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - if (!_wordWrap && CurrentColumn - _leftColumn > Viewport.Width) - { - _leftColumn = Math.Max (CurrentColumn - Viewport.Width + 1, 0); - } - - if (_wordWrap) - { - SetNeedsDraw (); - } - else - { - // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDraw (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0))); - SetNeedsDraw (); - } - - UpdateWrapModel (); - - OnContentsChanged (); - - return; - } - - List? rest = null; - var lastPosition = 0; - - if (_model.Count > 0 && line.Count > 0 && !_copyWithoutSelection) - { - // Keep a copy of the rest of the line - int restCount = line.Count - CurrentColumn; - rest = line.GetRange (CurrentColumn, restCount); - line.RemoveRange (CurrentColumn, restCount); - } - - // First line is inserted at the current location, the rest is appended - line.InsertRange (CurrentColumn, lines [0]); - - //model.AddLine (currentRow, lines [0]); - - List> addedLines = [new (line)]; - - for (var i = 1; i < lines.Count; i++) - { - _model.AddLine (CurrentRow + i, lines [i]); - - addedLines.Add ([.. lines [i]]); - } - - if (rest is { }) - { - List last = _model.GetLine (CurrentRow + lines.Count - 1); - lastPosition = last.Count; - last.InsertRange (last.Count, rest); - - addedLines.Last ().InsertRange (addedLines.Last ().Count, rest); - } - - _historyText.Add (addedLines, CursorPosition, TextEditingLineStatus.Added); - - // Now adjust column and row positions - CurrentRow += lines.Count - 1; - CurrentColumn = rest is { } ? lastPosition : lines [^1].Count; - Adjust (); - - _historyText.Add ( - [new (line)], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - OnContentsChanged (); - } - - private bool InsertText (Key a, Attribute? attribute = null) - { - //So that special keys like tab can be processed - if (_isReadOnly) - { - return true; - } - - SetWrapModel (); - - _historyText.Add ([new (GetCurrentLine ())], CursorPosition); - - if (IsSelecting) - { - ClearSelectedRegion (); - } - - if ((uint)a.KeyCode == '\n') - { - _model.AddLine (CurrentRow + 1, []); - CurrentRow++; - CurrentColumn = 0; - } - else if ((uint)a.KeyCode == '\r') - { - CurrentColumn = 0; - } - else - { - if (Used) - { - Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); - CurrentColumn++; - - if (CurrentColumn >= _leftColumn + Viewport.Width) - { - _leftColumn++; - SetNeedsDraw (); - } - } - else - { - Insert (new () { Grapheme = a.AsRune.ToString (), Attribute = attribute }); - CurrentColumn++; - } - } - - _historyText.Add ( - [new (GetCurrentLine ())], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - OnContentsChanged (); - - return true; - } - - private void KillToEndOfLine () - { - if (_isReadOnly) - { - return; - } - - if (_model.Count == 1 && GetCurrentLine ().Count == 0) - { - // Prevents from adding line feeds if there is no more lines. - return; - } - - SetWrapModel (); - - List currentLine = GetCurrentLine (); - var setLastWasKill = true; - - if (currentLine.Count > 0 && CurrentColumn == currentLine.Count) - { - UpdateWrapModel (); - - DeleteTextForwards (); - - return; - } - - _historyText.Add (new () { new (currentLine) }, CursorPosition); - - if (currentLine.Count == 0) - { - if (CurrentRow < _model.Count - 1) - { - List> removedLines = new () { new (currentLine) }; - - _model.RemoveLine (CurrentRow); - - removedLines.Add (new (GetCurrentLine ())); - - _historyText.Add ( - new (removedLines), - CursorPosition, - TextEditingLineStatus.Removed - ); - } - - if (_model.Count > 0 || _lastWasKill) - { - string val = Environment.NewLine; - - if (_lastWasKill) - { - AppendClipboard (val); - } - else - { - SetClipboard (val); - } - } - - if (_model.Count == 0) - { - // Prevents from adding line feeds if there is no more lines. - setLastWasKill = false; - } - } - else - { - int restCount = currentLine.Count - CurrentColumn; - List rest = currentLine.GetRange (CurrentColumn, restCount); - var val = string.Empty; - val += StringFromCells (rest); - - if (_lastWasKill) - { - AppendClipboard (val); - } - else - { - SetClipboard (val); - } - - currentLine.RemoveRange (CurrentColumn, restCount); - } - - _historyText.Add ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); - - _lastWasKill = setLastWasKill; - DoNeededAction (); - } - - private void KillToLeftStart () - { - if (_isReadOnly) - { - return; - } - - if (_model.Count == 1 && GetCurrentLine ().Count == 0) - { - // Prevents from adding line feeds if there is no more lines. - return; - } - - SetWrapModel (); - - List currentLine = GetCurrentLine (); - var setLastWasKill = true; - - if (currentLine.Count > 0 && CurrentColumn == 0) - { - UpdateWrapModel (); - - DeleteTextBackwards (); - - return; - } - - _historyText.Add ([ [.. currentLine]], CursorPosition); - - if (currentLine.Count == 0) - { - if (CurrentRow > 0) - { - _model.RemoveLine (CurrentRow); - - if (_model.Count > 0 || _lastWasKill) - { - string val = Environment.NewLine; - - if (_lastWasKill) - { - AppendClipboard (val); - } - else - { - SetClipboard (val); - } - } - - if (_model.Count == 0) - { - // Prevents from adding line feeds if there is no more lines. - setLastWasKill = false; - } - - CurrentRow--; - currentLine = _model.GetLine (CurrentRow); - - List> removedLine = - [ - [..currentLine], - [] - ]; - - _historyText.Add ( - [.. removedLine], - CursorPosition, - TextEditingLineStatus.Removed - ); - - CurrentColumn = currentLine.Count; - } - } - else - { - int restCount = CurrentColumn; - List rest = currentLine.GetRange (0, restCount); - var val = string.Empty; - val += StringFromCells (rest); - - if (_lastWasKill) - { - AppendClipboard (val); - } - else - { - SetClipboard (val); - } - - currentLine.RemoveRange (0, restCount); - CurrentColumn = 0; - } - - _historyText.Add ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); - - _lastWasKill = setLastWasKill; - DoNeededAction (); - } - - private void KillWordBackward () - { - if (_isReadOnly) - { - return; - } - - SetWrapModel (); - - List currentLine = GetCurrentLine (); - - _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); - - if (CurrentColumn == 0) - { - DeleteTextBackwards (); - - _historyText.ReplaceLast ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - return; - } - - (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); - - if (newPos.HasValue && CurrentRow == newPos.Value.row) - { - int restCount = CurrentColumn - newPos.Value.col; - currentLine.RemoveRange (newPos.Value.col, restCount); - - if (_wordWrap) - { - _wrapNeeded = true; - } - - CurrentColumn = newPos.Value.col; - } - else if (newPos.HasValue) - { - int restCount; - - if (newPos.Value.row == CurrentRow) - { - restCount = currentLine.Count - CurrentColumn; - currentLine.RemoveRange (CurrentColumn, restCount); - } - else - { - while (CurrentRow != newPos.Value.row) - { - restCount = currentLine.Count; - currentLine.RemoveRange (0, restCount); - - CurrentRow--; - currentLine = GetCurrentLine (); - } - } - - if (_wordWrap) - { - _wrapNeeded = true; - } - - CurrentColumn = newPos.Value.col; - CurrentRow = newPos.Value.row; - } - - _historyText.Add ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); - DoNeededAction (); - } - - private void KillWordForward () - { - if (_isReadOnly) - { - return; - } - - SetWrapModel (); - - List currentLine = GetCurrentLine (); - - _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); - - if (currentLine.Count == 0 || CurrentColumn == currentLine.Count) - { - DeleteTextForwards (); - - _historyText.ReplaceLast ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - return; - } - - (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); - var restCount = 0; - - if (newPos.HasValue && CurrentRow == newPos.Value.row) - { - restCount = newPos.Value.col - CurrentColumn; - currentLine.RemoveRange (CurrentColumn, restCount); - } - else if (newPos.HasValue) - { - restCount = currentLine.Count - CurrentColumn; - currentLine.RemoveRange (CurrentColumn, restCount); - } - - if (_wordWrap) - { - _wrapNeeded = true; - } - - _historyText.Add ( - [ [.. GetCurrentLine ()]], - CursorPosition, - TextEditingLineStatus.Replaced - ); - - UpdateWrapModel (); - - DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); - DoNeededAction (); - } - - private void Model_LinesLoaded (object sender, EventArgs e) - { - // This call is not needed. Model_LinesLoaded gets invoked when - // model.LoadString (value) is called. LoadString is called from one place - // (Text.set) and historyText.Clear() is called immediately after. - // If this call happens, HistoryText_ChangeText will get called multiple times - // when Text is set, which is wrong. - //historyText.Clear (Text); - - if (!_multiline && !IsInitialized) - { - CurrentColumn = Text.GetRuneCount (); - _leftColumn = CurrentColumn > Viewport.Width + 1 ? CurrentColumn - Viewport.Width + 1 : 0; - } - } - - private void MoveBottomEnd () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveEnd (); - } - - private void MoveBottomEndExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveEnd (); - } - - private bool MoveDown () - { - if (CurrentRow + 1 < _model.Count) - { - if (_columnTrack == -1) - { - _columnTrack = CurrentColumn; - } - - CurrentRow++; - - if (CurrentRow >= _topRow + Viewport.Height) - { - _topRow++; - SetNeedsDraw (); - } - - TrackColumn (); - PositionCursor (); - } - else if (CurrentRow > Viewport.Height) - { - Adjust (); - } - else - { - return false; - } - - DoNeededAction (); - - return true; - } - - private void MoveEndOfLine () - { - List currentLine = GetCurrentLine (); - CurrentColumn = currentLine.Count; - DoNeededAction (); - } - - private bool MoveLeft () - { - if (CurrentColumn > 0) - { - CurrentColumn--; - } - else - { - if (CurrentRow > 0) - { - CurrentRow--; - - if (CurrentRow < _topRow) - { - _topRow--; - SetNeedsDraw (); - } - - List currentLine = GetCurrentLine (); - CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0); - } - else - { - return false; - } - } - - DoNeededAction (); - - return true; - } - - private void MovePageDown () - { - int nPageDnShift = Viewport.Height - 1; - - if (CurrentRow >= 0 && CurrentRow < _model.Count) - { - if (_columnTrack == -1) - { - _columnTrack = CurrentColumn; - } - - CurrentRow = CurrentRow + nPageDnShift > _model.Count - ? _model.Count > 0 ? _model.Count - 1 : 0 - : CurrentRow + nPageDnShift; - - if (_topRow < CurrentRow - nPageDnShift) - { - _topRow = CurrentRow >= _model.Count - ? CurrentRow - nPageDnShift - : _topRow + nPageDnShift; - SetNeedsDraw (); - } - - TrackColumn (); - PositionCursor (); - } - - DoNeededAction (); - } - - private void MovePageUp () - { - int nPageUpShift = Viewport.Height - 1; - - if (CurrentRow > 0) - { - if (_columnTrack == -1) - { - _columnTrack = CurrentColumn; - } - - CurrentRow = CurrentRow - nPageUpShift < 0 ? 0 : CurrentRow - nPageUpShift; - - if (CurrentRow < _topRow) - { - _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift; - SetNeedsDraw (); - } - - TrackColumn (); - PositionCursor (); - } - - DoNeededAction (); - } - - private bool MoveRight () - { - List currentLine = GetCurrentLine (); - - if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count) - { - CurrentColumn++; - } - else - { - if (CurrentRow + 1 < _model.Count) - { - CurrentRow++; - CurrentColumn = 0; - - if (CurrentRow >= _topRow + Viewport.Height) - { - _topRow++; - SetNeedsDraw (); - } - } - else - { - return false; - } - } - - DoNeededAction (); - - return true; - } - - private void MoveLeftStart () - { - if (_leftColumn > 0) - { - SetNeedsDraw (); - } - - CurrentColumn = 0; - _leftColumn = 0; - DoNeededAction (); - } - - private void MoveTopHome () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveHome (); - } - - private void MoveTopHomeExtend () - { - ResetColumnTrack (); - StartSelecting (); - MoveHome (); - } - - private bool MoveUp () - { - if (CurrentRow > 0) - { - if (_columnTrack == -1) - { - _columnTrack = CurrentColumn; - } - - CurrentRow--; - - if (CurrentRow < _topRow) - { - _topRow--; - SetNeedsDraw (); - } - - TrackColumn (); - PositionCursor (); - } - else - { - return false; - } - - DoNeededAction (); - - return true; - } - - private void MoveWordBackward () - { - (int col, int row)? newPos = _model.WordBackward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); - - if (newPos.HasValue) - { - CurrentColumn = newPos.Value.col; - CurrentRow = newPos.Value.row; - } - - DoNeededAction (); - } - - private void MoveWordForward () - { - (int col, int row)? newPos = _model.WordForward (CurrentColumn, CurrentRow, UseSameRuneTypeForWords); - - if (newPos.HasValue) - { - CurrentColumn = newPos.Value.col; - CurrentRow = newPos.Value.row; - } - - DoNeededAction (); - } - - private (int width, int height) OffSetBackground () - { - var w = 0; - var h = 0; - - if (SuperView?.Viewport.Right - Viewport.Right < 0) - { - w = SuperView!.Viewport.Right - Viewport.Right - 1; - } - - if (SuperView?.Viewport.Bottom - Viewport.Bottom < 0) - { - h = SuperView!.Viewport.Bottom - Viewport.Bottom - 1; - } - - return (w, h); - } - - private bool PointInSelection (int col, int row) - { - long start, end; - GetEncodedRegionBounds (out start, out end); - long q = ((long)(uint)row << 32) | (uint)col; - - return q >= start && q <= end - 1; - } - private void ProcessAutocomplete () { if (_isDrawing) @@ -3886,7 +1035,7 @@ public class TextView : View, IDesignable List currentLine = GetCurrentLine (); - if (currentLine.Count > 0 && currentLine[CurrentColumn - 1].Grapheme == "\t") + if (currentLine.Count > 0 && currentLine [CurrentColumn - 1].Grapheme == "\t") { _historyText.Add (new () { new (currentLine) }, CursorPosition); @@ -3910,58 +1059,6 @@ public class TextView : View, IDesignable return true; } - private void ProcessCopy () - { - ResetColumnTrack (); - Copy (); - } - - private void ProcessCut () - { - ResetColumnTrack (); - Cut (); - } - - private void ProcessDeleteCharLeft () - { - ResetColumnTrack (); - DeleteCharLeft (); - } - - private void ProcessDeleteCharRight () - { - ResetColumnTrack (); - DeleteCharRight (); - } - - private Attribute? GetSelectedAttribute (int row, int col) - { - if (!InheritsPreviousAttribute || (Lines == 1 && GetLine (Lines).Count == 0)) - { - return null; - } - - List line = GetLine (row); - int foundRow = row; - - while (line.Count == 0) - { - if (foundRow == 0 && line.Count == 0) - { - return null; - } - - foundRow--; - line = GetLine (foundRow); - } - - int foundCol = foundRow < row ? line.Count - 1 : Math.Min (col, line.Count - 1); - - Cell cell = line [foundCol]; - - return cell.Attribute; - } - // If InheritsPreviousScheme is enabled this method will check if the rune cell on // the row and col location and around has a not null scheme. If it's null will set it with // the very most previous valid scheme. @@ -4073,19 +1170,6 @@ public class TextView : View, IDesignable } } - private void ProcessKillWordBackward () - { - ResetColumnTrack (); - KillWordBackward (); - } - - private void ProcessKillWordForward () - { - ResetColumnTrack (); - StopSelecting (); - KillWordForward (); - } - private void ProcessMouseClick (MouseEventArgs ev, out List line) { List? r = null; @@ -4119,245 +1203,7 @@ public class TextView : View, IDesignable line = r!; } - private bool ProcessMoveDown () - { - ResetContinuousFindTrack (); - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - return MoveDown (); - } - - private void ProcessMoveDownExtend () - { - ResetColumnTrack (); - StartSelecting (); - MoveDown (); - } - - private void ProcessMoveEndOfLine () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveEndOfLine (); - } - - private void ProcessMoveRightEndExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveEndOfLine (); - } - - private bool ProcessMoveLeft () - { - // if the user presses Left (without any control keys) and they are at the start of the text - if (CurrentColumn == 0 && CurrentRow == 0) - { - if (IsSelecting) - { - StopSelecting (); - - return true; - } - - // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward) - return false; - } - - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveLeft (); - - return true; - } - - private void ProcessMoveLeftExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveLeft (); - } - - private bool ProcessMoveRight () - { - // if the user presses Right (without any control keys) - // determine where the last cursor position in the text is - int lastRow = _model.Count - 1; - int lastCol = _model.GetLine (lastRow).Count; - - // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward) - if (CurrentColumn == lastCol && CurrentRow == lastRow) - { - // Unless they have text selected - if (IsSelecting) - { - // In which case clear - StopSelecting (); - - return true; - } - - return false; - } - - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveRight (); - - return true; - } - - private void ProcessMoveRightExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveRight (); - } - - private void ProcessMoveLeftStart () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveLeftStart (); - } - - private void ProcessMoveLeftStartExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveLeftStart (); - } - - private bool ProcessMoveUp () - { - ResetContinuousFindTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - return MoveUp (); - } - - private void ProcessMoveUpExtend () - { - ResetColumnTrack (); - StartSelecting (); - MoveUp (); - } - - private void ProcessMoveWordBackward () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveWordBackward (); - } - - private void ProcessMoveWordBackwardExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveWordBackward (); - } - - private void ProcessMoveWordForward () - { - ResetAllTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MoveWordForward (); - } - - private void ProcessMoveWordForwardExtend () - { - ResetAllTrack (); - StartSelecting (); - MoveWordForward (); - } - - private void ProcessPageDown () - { - ResetColumnTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MovePageDown (); - } - - private void ProcessPageDownExtend () - { - ResetColumnTrack (); - StartSelecting (); - MovePageDown (); - } - - private void ProcessPageUp () - { - ResetColumnTrack (); - - if (_shiftSelecting && IsSelecting) - { - StopSelecting (); - } - - MovePageUp (); - } - - private void ProcessPageUpExtend () - { - ResetColumnTrack (); - StartSelecting (); - MovePageUp (); - } - - private void ProcessPaste () - { - ResetColumnTrack (); - - if (_isReadOnly) - { - return; - } - - Paste (); - } private bool ProcessEnterKey (ICommandContext? commandContext) { @@ -4442,12 +1288,6 @@ public class TextView : View, IDesignable return true; } - private void ProcessSelectAll () - { - ResetColumnTrack (); - SelectAll (); - } - private void ProcessSetOverwrite () { ResetColumnTrack (); @@ -4469,105 +1309,7 @@ public class TextView : View, IDesignable return true; } - private void ResetAllTrack () - { - // Handle some state here - whether the last command was a kill - // operation and the column tracking (up/down) - _lastWasKill = false; - _columnTrack = -1; - _continuousFind = false; - } - private void ResetColumnTrack () - { - // Handle some state here - whether the last command was a kill - // operation and the column tracking (up/down) - _lastWasKill = false; - _columnTrack = -1; - } - - private void ResetContinuousFind () - { - if (!_continuousFind) - { - int col = IsSelecting ? _selectionStartColumn : CurrentColumn; - int row = IsSelecting ? _selectionStartRow : CurrentRow; - _model.ResetContinuousFind (new (col, row)); - } - } - - private void ResetContinuousFindTrack () - { - // Handle some state here - whether the last command was a kill - // operation and the column tracking (up/down) - _lastWasKill = false; - _continuousFind = false; - } - - private void ResetPosition () - { - _topRow = _leftColumn = CurrentRow = CurrentColumn = 0; - StopSelecting (); - } - - private void SetClipboard (string text) - { - if (text is { }) - { - Clipboard.Contents = text; - } - } - - private bool SetFoundText ( - string text, - (Point current, bool found) foundPos, - string? textToReplace = null, - bool replace = false, - bool replaceAll = false - ) - { - if (foundPos.found) - { - StartSelecting (); - _selectionStartColumn = foundPos.current.X; - _selectionStartRow = foundPos.current.Y; - - if (!replaceAll) - { - CurrentColumn = _selectionStartColumn + text.GetRuneCount (); - } - else - { - CurrentColumn = _selectionStartColumn + textToReplace!.GetRuneCount (); - } - - CurrentRow = foundPos.current.Y; - - if (!_isReadOnly && replace) - { - Adjust (); - ClearSelectedRegion (); - InsertAllText (textToReplace!); - StartSelecting (); - _selectionStartColumn = CurrentColumn - textToReplace!.GetRuneCount (); - } - else - { - UpdateWrapModel (); - SetNeedsDraw (); - Adjust (); - } - - _continuousFind = true; - - return foundPos.found; - } - - UpdateWrapModel (); - _continuousFind = false; - - return foundPos.found; - } private void SetOverwrite (bool overwrite) { @@ -4583,305 +1325,4 @@ public class TextView : View, IDesignable SetAttribute (new (attribute!.Value.Background, attribute!.Value.Foreground, attribute!.Value.Style)); } - /// Restore from original model. - private void SetWrapModel ([CallerMemberName] string? caller = null) - { - if (_currentCaller is { }) - { - return; - } - - if (_wordWrap) - { - _currentCaller = caller; - - CurrentColumn = _wrapManager!.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); - CurrentRow = _wrapManager.GetModelLineFromWrappedLines (CurrentRow); - - _selectionStartColumn = - _wrapManager.GetModelColFromWrappedLines (_selectionStartRow, _selectionStartColumn); - _selectionStartRow = _wrapManager.GetModelLineFromWrappedLines (_selectionStartRow); - _model = _wrapManager.Model; - } - } - - private void ShowContextMenu (Point? mousePosition) - { - if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) - { - _currentCulture = Thread.CurrentThread.CurrentUICulture; - } - - if (mousePosition is null) - { - mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y)); - } - - ContextMenu?.MakeVisible (mousePosition); - } - - private void StartSelecting () - { - if (_shiftSelecting && IsSelecting) - { - return; - } - - _shiftSelecting = true; - IsSelecting = true; - _selectionStartColumn = CurrentColumn; - _selectionStartRow = CurrentRow; - } - - private void StopSelecting () - { - if (IsSelecting) - { - SetNeedsDraw (); - } - - _shiftSelecting = false; - IsSelecting = false; - _isButtonShift = false; - } - - private string StringFromCells (List cells) - { - ArgumentNullException.ThrowIfNull (cells); - - var size = 0; - foreach (Cell cell in cells) - { - string t = cell.Grapheme; - size += Encoding.Unicode.GetByteCount (t); - } - - byte [] encoded = new byte [size]; - var offset = 0; - foreach (Cell cell in cells) - { - string t = cell.Grapheme; - int bytesWritten = Encoding.Unicode.GetBytes (t, 0, t.Length, encoded, offset); - offset += bytesWritten; - } - - // decode using the same encoding and the bytes actually written - return Encoding.Unicode.GetString (encoded, 0, offset); - } - - private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e) - { - if (e.SuperView is { }) - { - if (Autocomplete.HostControl is null) - { - Autocomplete.HostControl = this; - } - } - else - { - Autocomplete.HostControl = null; - } - } - - private void TextView_Initialized (object sender, EventArgs e) - { - if (Autocomplete.HostControl is null) - { - Autocomplete.HostControl = this; - } - - - ContextMenu = CreateContextMenu (); - App?.Popover?.Register (ContextMenu); - KeyBindings.Add (ContextMenu.Key, Command.Context); - - // Configure ScrollBars to use modern View scrolling infrastructure - ConfigureScrollBars (); - - OnContentsChanged (); - } - - /// - /// Configures the ScrollBars to work with the modern View scrolling system. - /// - private void ConfigureScrollBars () - { - // Subscribe to ViewportChanged to sync internal scroll fields - ViewportChanged += TextView_ViewportChanged; - - // Vertical ScrollBar: AutoShow enabled by default as per requirements - VerticalScrollBar.AutoShow = true; - - // Horizontal ScrollBar: AutoShow tracks WordWrap as per requirements - HorizontalScrollBar.AutoShow = !WordWrap; - } - - private void TextView_ViewportChanged (object? sender, DrawEventArgs e) - { - // Sync internal scroll position fields with Viewport - // Only update if values actually changed to prevent infinite loops - if (_topRow != Viewport.Y) - { - _topRow = Viewport.Y; - } - - if (_leftColumn != Viewport.X) - { - _leftColumn = Viewport.X; - } - } - - /// - /// Updates the content size based on the text model dimensions. - /// - private void UpdateContentSize () - { - int contentHeight = Math.Max (_model.Count, 1); - - // For horizontal size: if word wrap is enabled, content width equals viewport width - // Otherwise, calculate the maximum line width (but only if we have a reasonable viewport) - int contentWidth; - - if (_wordWrap) - { - // Word wrap: content width follows viewport width - contentWidth = Math.Max (Viewport.Width, 1); - } - else - { - // No word wrap: calculate max line width - // Cache the current value to avoid recalculating on every call - contentWidth = Math.Max (_model.GetMaxVisibleLine (0, _model.Count, TabWidth), 1); - } - - SetContentSize (new Size (contentWidth, contentHeight)); - } - - private void TextView_LayoutComplete (object? sender, LayoutEventArgs e) - { - WrapTextModel (); - UpdateContentSize (); - Adjust (); - } - - private void ToggleSelecting () - { - ResetColumnTrack (); - IsSelecting = !IsSelecting; - _selectionStartColumn = CurrentColumn; - _selectionStartRow = CurrentRow; - } - - // Tries to snap the cursor to the tracking column - private void TrackColumn () - { - // Now track the column - List line = GetCurrentLine (); - - if (line.Count < _columnTrack) - { - CurrentColumn = line.Count; - } - else if (_columnTrack != -1) - { - CurrentColumn = _columnTrack; - } - else if (CurrentColumn > line.Count) - { - CurrentColumn = line.Count; - } - - Adjust (); - } - - /// Update the original model. - private void UpdateWrapModel ([CallerMemberName] string? caller = null) - { - if (_currentCaller is { } && _currentCaller != caller) - { - return; - } - - if (_wordWrap) - { - _currentCaller = null; - - _wrapManager!.UpdateModel ( - _model, - out int nRow, - out int nCol, - out int nStartRow, - out int nStartCol, - CurrentRow, - CurrentColumn, - _selectionStartRow, - _selectionStartColumn, - true - ); - CurrentRow = nRow; - CurrentColumn = nCol; - _selectionStartRow = nStartRow; - _selectionStartColumn = nStartCol; - _wrapNeeded = true; - - SetNeedsDraw (); - } - - if (_currentCaller is { }) - { - throw new InvalidOperationException ( - $"WordWrap settings was changed after the {_currentCaller} call." - ); - } - } - - private void WrapTextModel () - { - if (_wordWrap && _wrapManager is { }) - { - _model = _wrapManager.WrapModel ( - Math.Max (Viewport.Width - (ReadOnly ? 0 : 1), 0), // For the cursor on the last column of a line - out int nRow, - out int nCol, - out int nStartRow, - out int nStartCol, - CurrentRow, - CurrentColumn, - _selectionStartRow, - _selectionStartColumn, - _tabWidth - ); - CurrentRow = nRow; - CurrentColumn = nCol; - _selectionStartRow = nStartRow; - _selectionStartColumn = nStartCol; - SetNeedsDraw (); - } - } - - /// - public bool EnableForDesign () - { - Text = """ - TextView provides a fully featured multi-line text editor. - It supports word wrap and history for undo. - """; - - return true; - } - - - /// - protected override void Dispose (bool disposing) - { - if (disposing && ContextMenu is { }) - { - ContextMenu.Visible = false; - ContextMenu.Dispose (); - ContextMenu = null; - } - - base.Dispose (disposing); - } } \ No newline at end of file diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index ef25662a4..0662bf5a6 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -1,7 +1,9 @@  BackingField Inherit + ReturnDefaultValue True + True 5000 1000 3000 @@ -14,7 +16,7 @@ SUGGESTION WARNING ERROR - ERROR + SUGGESTION WARNING SUGGESTION WARNING @@ -331,6 +333,7 @@ <Entry.SortBy> <Access Is="0" /> <Readonly /> + <PropertyName /> </Entry.SortBy> </Entry> <Property DisplayName="Properties w/ Backing Field" Priority="100"> @@ -353,14 +356,18 @@ </And> </Entry.Match> <Entry.SortBy> - <ImplementsInterface Immediate="True" /> + <ImplementsInterface /> + <Name /> </Entry.SortBy> </Entry> <Entry DisplayName="All other members"> <Entry.SortBy> <Access Is="0" /> - <Name /> + <Static /> + <Virtual /> <Override /> + <ImplementsInterface /> + <Name /> </Entry.SortBy> </Entry> <Entry DisplayName="Nested Types"> @@ -374,10 +381,11 @@ </Entry> </TypePattern> </Patterns> - UseVarWhenEvident + UseExplicitType UseExplicitType - UseVarWhenEvident + UseVar True + True False False True