From 0669e2cc944f85169bc2ae2da4a51e3c99504c9a Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 1 Apr 2021 17:14:14 +0100 Subject: [PATCH] Fixes #134. TextView: Add line wrapping. (#1147) * Fixes #134. TextView: Add line wrapping. * Facilitates exception handling in Debug mode. * Fixes the cursor position if it is always on the same line. * Fixed TextView column position and hiding the cursor if it is outside outbound frame. * Added more wrap lines features.. Added a BottomOffset property. Fixed Ctrl+K and Ctrl+Y. * Fixes the Text property and fixing a redraw behavior not cleaning well at right. * Fixes Ctrl+K from adding an extra line feed. * Implemented Ctrl+K and Ctrl-Y on wrapped lines and preventing left Column to be greater than 0 on wrap. * Fixes more line feed issues. * More line feed fixing. * Fixes UpdateModel that must return the new row and new column from the wrapped lines. * Fixes #1155. MoveForward/MoveBackward not bound on Text controls. * Added much more features to TextView. * Fixes ResetPosition and forcing the cursor visibility. * Implemented the New, Save and SaveAs methods in the Editor. --- Terminal.Gui/Core/Application.cs | 10 +- Terminal.Gui/Core/View.cs | 4 + Terminal.Gui/Views/TextField.cs | 8 +- Terminal.Gui/Views/TextView.cs | 896 +++++++++++++++++++++++++++---- UICatalog/Scenarios/Editor.cs | 142 ++++- 5 files changed, 932 insertions(+), 128 deletions(-) diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index ccae71e6d..0d37fda83 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -702,8 +702,13 @@ namespace Terminal.Gui { var resume = true; while (resume) { - try - { +#if DEBUG + resume = false; + var runToken = Begin (view); + RunLoop (runToken); + End (runToken); +#else + try { resume = false; var runToken = Begin (view); RunLoop (runToken); @@ -717,6 +722,7 @@ namespace Terminal.Gui { } resume = errorHandler(error); } +#endif } } diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index cefddbe3a..d6809dd64 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1471,6 +1471,8 @@ namespace Terminal.Gui { KeyPress?.Invoke (args); if (args.Handled) return true; + if (Focused?.ProcessKey (keyEvent) == true) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) @@ -1486,6 +1488,8 @@ namespace Terminal.Gui { KeyPress?.Invoke (args); if (args.Handled) return true; + if (Focused?.ProcessKey (keyEvent) == true) + return true; if (subviews == null || subviews.Count == 0) return false; foreach (var view in subviews) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index b24054087..3c81a36c6 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -389,6 +389,7 @@ namespace Terminal.Gui { case Key.CursorLeft | Key.ShiftMask | Key.CtrlMask: case Key.CursorUp | Key.ShiftMask | Key.CtrlMask: + case (Key)((int)'B' + Key.ShiftMask | Key.AltMask): if (point > 0) { int x = start > -1 && start > point ? start : point; if (x > 0) { @@ -402,6 +403,7 @@ namespace Terminal.Gui { case Key.CursorRight | Key.ShiftMask | Key.CtrlMask: case Key.CursorDown | Key.ShiftMask | Key.CtrlMask: + case (Key)((int)'F' + Key.ShiftMask | Key.AltMask): if (point < text.Count) { int x = start > -1 && start > point ? start : point; int sfw = WordForward (x); @@ -503,7 +505,7 @@ namespace Terminal.Gui { case Key.CursorLeft | Key.CtrlMask: case Key.CursorUp | Key.CtrlMask: - case (Key)((int)'b' + Key.AltMask): + case (Key)((int)'B' + Key.AltMask): ClearAllSelection (); int bw = WordBackward (point); if (bw != -1) @@ -513,7 +515,7 @@ namespace Terminal.Gui { case Key.CursorRight | Key.CtrlMask: case Key.CursorDown | Key.CtrlMask: - case (Key)((int)'f' + Key.AltMask): + case (Key)((int)'F' + Key.AltMask): ClearAllSelection (); int fw = WordForward (point); if (fw != -1) @@ -604,7 +606,7 @@ namespace Terminal.Gui { } } if (i != p) - return i; + return i + 1; return -1; } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 60fe9dc8c..a53274427 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -79,8 +79,7 @@ namespace Terminal.Gui { { var lines = new List> (); int start = 0, i = 0; - // BUGBUG: I think this is buggy w.r.t Unicode. content.Length is bytes, and content[i] is bytes - // and content[i] == 10 may be the middle of a Rune. + // ASCII code 10 = Line Feed. for (; i < content.Length; i++) { if (content [i] == 10) { if (i - start > 0) @@ -131,7 +130,7 @@ namespace Terminal.Gui { { var sb = new StringBuilder (); for (int i = 0; i < lines.Count; i++) { - sb.Append (ustring.Make (lines[i])); + sb.Append (ustring.Make (lines [i])); if ((i + 1) < lines.Count) { sb.AppendLine (); } @@ -151,7 +150,19 @@ namespace Terminal.Gui { /// /// The line. /// Line number to retrieve. - public List GetLine (int line) => line < Count ? lines [line] : lines [Count - 1]; + public List GetLine (int line) + { + if (lines.Count > 0) { + if (line < Count) { + return lines [line]; + } else { + return lines [Count - 1]; + } + } else { + lines.Add (new List ()); + return lines [0]; + } + } /// /// Adds a line to the model at the specified position. @@ -169,7 +180,9 @@ namespace Terminal.Gui { /// Position. public void RemoveLine (int pos) { - lines.RemoveAt (pos); + if (lines.Count > 0) { + lines.RemoveAt (pos); + } } /// @@ -268,6 +281,246 @@ namespace Terminal.Gui { } } + class WordWrapManager { + class WrappedLine { + public int ModelLine; + public int Row; + public int RowIndex; + public int ColWidth; + } + + List wrappedModelLines = new List (); + int frameWidth; + bool isWrapModelRefreshing; + + public TextModel Model { get; private set; } + + public WordWrapManager (TextModel model) + { + Model = model; + } + + public TextModel WrapModel (int width, out int nRow, out int nCol, out int nStartRow, out int nStartCol, + int row = 0, int col = 0, int startRow = 0, int startCol = 0) + { + frameWidth = width; + + var modelRow = isWrapModelRefreshing ? row : GetModelLineFromWrappedLines (row); + var modelCol = isWrapModelRefreshing ? col : GetModelColFromWrappedLines (row, col); + var modelStartRow = isWrapModelRefreshing ? startRow : GetModelLineFromWrappedLines (startRow); + var modelStartCol = isWrapModelRefreshing ? startCol : GetModelColFromWrappedLines (startRow, startCol); + var wrappedModel = new TextModel (); + int lines = 0; + nRow = 0; + nCol = 0; + nStartRow = 0; + nStartCol = 0; + bool isRowAndColSetted = row == 0 && col == 0; + bool isStartRowAndColSetted = startRow == 0 && startCol == 0; + List wModelLines = new List (); + + for (int i = 0; i < Model.Count; i++) { + var line = Model.GetLine (i); + var wrappedLines = ToListRune ( + TextFormatter.Format (ustring.Make (line), width, TextAlignment.Left, true, true)); + int sumColWidth = 0; + for (int j = 0; j < wrappedLines.Count; j++) { + var wrapLine = wrappedLines [j]; + if (!isRowAndColSetted && modelRow == i) { + if (nCol + wrapLine.Count <= modelCol) { + nCol += wrapLine.Count; + nRow = lines; + if (nCol == modelCol) { + nCol = wrapLine.Count; + isRowAndColSetted = true; + } else if (j == wrappedLines.Count - 1) { + nCol = wrapLine.Count - j + modelCol - nCol; + isRowAndColSetted = true; + } + } else { + var offset = nCol + wrapLine.Count - modelCol; + nCol = wrapLine.Count - offset; + nRow = lines; + isRowAndColSetted = true; + } + } + if (!isStartRowAndColSetted && modelStartRow == i) { + if (nStartCol + wrapLine.Count <= modelStartCol) { + nStartCol += wrapLine.Count; + nStartRow = lines; + if (nStartCol == modelStartCol) { + nStartCol = wrapLine.Count; + isStartRowAndColSetted = true; + } else if (j == wrappedLines.Count - 1) { + nStartCol = wrapLine.Count - j + modelStartCol - nStartCol; + isStartRowAndColSetted = true; + } + } else { + var offset = nStartCol + wrapLine.Count - modelStartCol; + nStartCol = wrapLine.Count - offset; + nStartRow = lines; + isStartRowAndColSetted = true; + } + } + wrappedModel.AddLine (lines, wrapLine); + sumColWidth += wrapLine.Count; + var wrappedLine = new WrappedLine () { + ModelLine = i, + Row = lines, + RowIndex = j, + ColWidth = wrapLine.Count, + }; + wModelLines.Add (wrappedLine); + lines++; + } + } + wrappedModelLines = wModelLines; + + return wrappedModel; + } + + public List> ToListRune (List textList) + { + var runesList = new List> (); + + foreach (var text in textList) { + runesList.Add (text.ToRuneList ()); + } + + return runesList; + } + + public int GetModelLineFromWrappedLines (int line) => wrappedModelLines.Count > 0 + ? wrappedModelLines [Math.Min (line, wrappedModelLines.Count - 1)].ModelLine + : 0; + + public int GetModelColFromWrappedLines (int line, int col) + { + if (wrappedModelLines?.Count == 0) { + return 0; + } + + var modelLine = GetModelLineFromWrappedLines (line); + var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); + int modelCol = 0; + + for (int i = firstLine; i <= line; i++) { + var wLine = wrappedModelLines [i]; + + if (i < line) { + modelCol += wLine.ColWidth; + } else { + modelCol += col; + } + } + + return modelCol; + } + + List GetCurrentLine (int row) => Model.GetLine (row); + + public void AddLine (int row, int col) + { + var modelRow = GetModelLineFromWrappedLines (row); + var modelCol = GetModelColFromWrappedLines (row, col); + var line = GetCurrentLine (modelRow); + var restCount = line.Count - modelCol; + var rest = line.GetRange (modelCol, restCount); + line.RemoveRange (modelCol, restCount); + Model.AddLine (modelRow + 1, rest); + isWrapModelRefreshing = true; + WrapModel (frameWidth, out _, out _, out _, out _, modelRow + 1, 0); + isWrapModelRefreshing = false; + } + + public bool Insert (int row, int col, Rune rune) + { + var line = GetCurrentLine (GetModelLineFromWrappedLines (row)); + line.Insert (GetModelColFromWrappedLines (row, col), rune); + if (line.Count > frameWidth) { + return true; + } else { + return false; + } + } + + public bool RemoveAt (int row, int col) + { + var modelRow = GetModelLineFromWrappedLines (row); + var line = GetCurrentLine (modelRow); + var modelCol = GetModelColFromWrappedLines (row, col); + + if (modelCol >= line.Count) { + Model.RemoveLine (modelRow); + RemoveAt (row, 0); + return false; + } + line.RemoveAt (modelCol); + if (line.Count > frameWidth || (row + 1 < wrappedModelLines.Count + && wrappedModelLines [row + 1].ModelLine == modelRow)) { + return true; + } + + return false; + } + + public bool RemoveLine (int row, int col, out bool lineRemoved, bool forward = true) + { + lineRemoved = false; + var modelRow = GetModelLineFromWrappedLines (row); + var line = GetCurrentLine (modelRow); + var modelCol = GetModelColFromWrappedLines (row, col); + + if (modelCol == 0 && line.Count == 0) { + Model.RemoveLine (modelRow); + return false; + } else if (modelCol < line.Count) { + if (forward) { + line.RemoveAt (modelCol); + return true; + } else if (modelCol - 1 > -1) { + line.RemoveAt (modelCol - 1); + return true; + } + } + lineRemoved = true; + if (forward) { + if (modelRow + 1 == Model.Count) { + return false; + } + + var nextLine = Model.GetLine (modelRow + 1); + line.AddRange (nextLine); + Model.RemoveLine (modelRow + 1); + if (line.Count > frameWidth) { + return true; + } + } else { + if (modelRow == 0) { + return false; + } + + var prevLine = Model.GetLine (modelRow - 1); + prevLine.AddRange (line); + Model.RemoveLine (modelRow); + if (prevLine.Count > frameWidth) { + return true; + } + } + + return false; + } + + public void UpdateModel (TextModel model, out int nRow, out int nCol, out int nStartRow, out int nStartCol, + int row, int col, int startRow, int startCol) + { + isWrapModelRefreshing = true; + Model = model; + WrapModel (frameWidth, out nRow, out nCol, out nStartRow, out nStartCol, row, col, startRow, startCol); + isWrapModelRefreshing = false; + } + } + /// /// Multi-line text editing /// @@ -393,6 +646,8 @@ namespace Terminal.Gui { int selectionStartColumn, selectionStartRow; bool selecting; //bool used; + bool wordWrap; + WordWrapManager wrapManager; /// /// Raised when the of the changes. @@ -431,6 +686,9 @@ namespace Terminal.Gui { void ResetPosition () { topRow = leftColumn = currentRow = currentColumn = 0; + selecting = false; + shiftSelecting = false; + ResetCursorVisibility (); } /// @@ -440,12 +698,20 @@ namespace Terminal.Gui { /// public override ustring Text { get { - return model.ToString (); + if (wordWrap) { + return wrapManager.Model.ToString (); + } else { + return model.ToString (); + } } set { ResetPosition (); model.LoadString (value); + if (wordWrap) { + wrapManager = new WordWrapManager (model); + model = wrapManager.WrapModel (Frame.Width - 2, out _, out _, out _, out _); + } TextChanged?.Invoke (); SetNeedsDisplay (); } @@ -456,10 +722,27 @@ namespace Terminal.Gui { get => base.Frame; set { base.Frame = value; + WrapTextModel (); Adjust (); } } + void WrapTextModel () + { + if (wordWrap && wrapManager != null) { + model = wrapManager.WrapModel (Frame.Width - 2, + out int nRow, out int nCol, + out int nStartRow, out int nStartCol, + currentRow, currentColumn, + selectionStartRow, selectionStartColumn); + currentRow = nRow; + currentColumn = nCol; + selectionStartRow = nStartRow; + selectionStartColumn = nStartCol; + SetNeedsDisplay (); + } + } + /// /// Gets or sets the top row. /// @@ -480,6 +763,59 @@ namespace Terminal.Gui { /// public int Lines => model.Count; + /// + /// Allows word wrap the to fit the available container width. + /// + public bool WordWrap { + get => wordWrap; + set { + if (value == wordWrap) { + return; + } + wordWrap = value; + ResetPosition (); + if (wordWrap) { + wrapManager = new WordWrapManager (model); + model = wrapManager.WrapModel (Frame.Width - 2, out _, out _, out _, out _); + } else if (!wordWrap && wrapManager != null) { + model = wrapManager.Model; + } + SetNeedsDisplay (); + } + } + + /// + /// The bottom offset needed to use a horizontal scrollbar or for another reason. + /// This is only needed with the keyboard navigation. + /// + public int BottomOffset { get; set; } + + /// + /// The right offset needed to use a vertical scrollbar or for another reason. + /// This is only needed with the keyboard navigation. + /// + public int RightOffset { get; set; } + + CursorVisibility savedCursorVisibility = CursorVisibility.Default; + + void SaveCursorVisibility () + { + if (desiredCursorVisibility != CursorVisibility.Invisible) { + savedCursorVisibility = desiredCursorVisibility; + DesiredCursorVisibility = CursorVisibility.Invisible; + } + } + + void ResetCursorVisibility () + { + if (savedCursorVisibility != desiredCursorVisibility) { + DesiredCursorVisibility = savedCursorVisibility; + savedCursorVisibility = CursorVisibility.Default; + } else { + DesiredCursorVisibility = CursorVisibility.Underline; + } + } + /// /// Loads the contents of the file into the . /// @@ -547,19 +883,22 @@ namespace Terminal.Gui { var retreat = 0; var col = 0; if (line.Count > 0) { - retreat = Math.Max ((SpecialRune (line [Math.Max (CurrentColumn - leftColumn - 1, 0)]) - ? 1 : 0), 0); + retreat = Math.Max (SpecialRune (line [Math.Min (Math.Max (currentColumn - leftColumn - 1, 0), line.Count - 1)]) + ? 1 : 0, 0); for (int idx = leftColumn < 0 ? 0 : leftColumn; idx < line.Count; idx++) { - if (idx == CurrentColumn) + if (idx == currentColumn) break; var cols = Rune.ColumnWidth (line [idx]); col += cols - 1; } } - var ccol = CurrentColumn - leftColumn - retreat + col; - if (leftColumn <= CurrentColumn && ccol < Frame.Width - && topRow <= CurrentRow && CurrentRow - topRow < Frame.Height) { - Move (ccol, CurrentRow - topRow); + var ccol = currentColumn - leftColumn - retreat + col; + if (leftColumn <= currentColumn && ccol < Frame.Width + && topRow <= currentRow && currentRow - topRow < Frame.Height) { + ResetCursorVisibility (); + Move (ccol, currentRow - topRow); + } else { + SaveCursorVisibility (); } } @@ -579,10 +918,7 @@ namespace Terminal.Gui { void ColorSelection () { - if (HasFocus) - Driver.SetAttribute (ColorScheme.Focus); - else - Driver.SetAttribute (ColorScheme.Normal); + Driver.SetAttribute (ColorScheme.Focus); } bool isReadOnly = false; @@ -598,17 +934,16 @@ namespace Terminal.Gui { } } - private CursorVisibility desiredCursorVisibility = CursorVisibility.Default; + CursorVisibility desiredCursorVisibility = CursorVisibility.Default; /// /// Get / Set the wished cursor when the field is focused /// - public CursorVisibility DesiredCursorVisibility - { - get => desiredCursorVisibility; + public CursorVisibility DesiredCursorVisibility { + get => desiredCursorVisibility; set { if (desiredCursorVisibility != value && HasFocus) { - Application.Driver.SetCursorVisibility (value); + Application.Driver.SetCursorVisibility (value); } desiredCursorVisibility = value; @@ -643,7 +978,7 @@ namespace Terminal.Gui { long start, end; GetEncodedRegionBounds (out start, out end); var q = ((long)(uint)row << 32) | (uint)col; - return q >= start && q <= end; + return q >= start && q <= end - 1; } // @@ -654,6 +989,9 @@ namespace Terminal.Gui { { long start, end; GetEncodedRegionBounds (out start, out end); + if (start == end) { + return ustring.Empty; + } int startRow = (int)(start >> 32); var maxrow = ((int)(end >> 32)); int startCol = (int)(start & 0xffffffff); @@ -661,7 +999,7 @@ namespace Terminal.Gui { var line = model.GetLine (startRow); if (startRow == maxrow) - return StringFromRunes (line.GetRange (startCol, endCol)); + return StringFromRunes (line.GetRange (startCol, endCol - startCol)); ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol)); @@ -690,7 +1028,11 @@ namespace Terminal.Gui { if (startRow == maxrow) { line.RemoveRange (startCol, endCol - startCol); currentColumn = startCol; - SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1)); + if (wordWrap) { + SetNeedsDisplay (); + } else { + SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1)); + } return; } @@ -708,13 +1050,45 @@ namespace Terminal.Gui { SetNeedsDisplay (); } + /// + /// Restore from original model. + /// + void SetWrapModel () + { + if (wordWrap) { + 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. + /// + void UpdateWrapModel () + { + if (wordWrap) { + wrapManager.UpdateModel (model, out int nRow, out int nCol, + out int nStartRow, out int nStartCol, + currentRow, currentColumn, + selectionStartRow, selectionStartColumn); + currentRow = nRow; + currentColumn = nCol; + selectionStartRow = nStartRow; + selectionStartColumn = nStartCol; + wrapNeeded = true; + } + } + /// public override void Redraw (Rect bounds) { ColorNormal (); int bottom = bounds.Bottom; - int right = bounds.Right; + int right = bounds.Right + 1; for (int row = bounds.Top; row < bottom; row++) { int textLine = topRow + row; if (textLine >= model.Count) { @@ -735,7 +1109,7 @@ namespace Terminal.Gui { var lineCol = leftColumn + idx; var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol]; var cols = Rune.ColumnWidth (rune); - if (selecting && PointInSelection (idx, row)) { + if (lineCol < line.Count && selecting && PointInSelection (idx + leftColumn, row + topRow)) { ColorSelection (); } else { ColorNormal (); @@ -757,7 +1131,8 @@ namespace Terminal.Gui { case 0xd: return true; default: - return false; } + return false; + } } /// @@ -768,21 +1143,30 @@ namespace Terminal.Gui { void SetClipboard (ustring text) { - Clipboard.Contents = text; + if (!text.IsEmpty) { + Clipboard.Contents = text; + } } void AppendClipboard (ustring text) { - Clipboard.Contents = Clipboard.Contents + text; + Clipboard.Contents += text; } void Insert (Rune rune) { var line = GetCurrentLine (); - line.Insert (currentColumn, rune); + line.Insert (Math.Min (currentColumn, line.Count), rune); + if (wordWrap) { + wrapNeeded = wrapManager.Insert (currentRow, currentColumn, rune); + if (wrapNeeded) { + SetNeedsDisplay (); + } + } var prow = currentRow - topRow; - - SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1)); + if (!wrapNeeded) { + SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1)); + } } ustring StringFromRunes (List runes) @@ -821,32 +1205,44 @@ namespace Terminal.Gui { if (lines.Count == 1) { line.InsertRange (currentColumn, lines [0]); currentColumn += lines [0].Count; - if (currentColumn - leftColumn > Frame.Width) { - leftColumn = currentColumn - Frame.Width + 1; + if (!wordWrap && currentColumn - leftColumn > Frame.Width) { + leftColumn = Math.Max (currentColumn - Frame.Width + 1, 0); + } + if (wordWrap) { + SetNeedsDisplay (); + } else { + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, currentRow - topRow + 1)); } - SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, currentRow - topRow + 1)); return; } - // Keep a copy of the rest of the line - var restCount = line.Count - currentColumn; - var rest = line.GetRange (currentColumn, restCount); - line.RemoveRange (currentColumn, restCount); + List rest = null; + int lastp = 0; + + if (model.Count > 0 && currentColumn > 0) { + // Keep a copy of the rest of the line + var 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]); for (int i = 1; i < lines.Count; i++) { model.AddLine (currentRow + i, lines [i]); } - var last = model.GetLine (currentRow + lines.Count - 1); - var lastp = last.Count; - last.InsertRange (last.Count, rest); + if (rest != null) { + var last = model.GetLine (currentRow + lines.Count - 1); + lastp = last.Count; + last.InsertRange (last.Count, rest); + } // Now adjust column and row positions currentRow += lines.Count - 1; - currentColumn = lastp; + currentColumn = rest != null ? lastp : lines [lines.Count - 1].Count; Adjust (); } @@ -871,24 +1267,28 @@ namespace Terminal.Gui { { var offB = OffSetBackground (); var line = GetCurrentLine (); - bool need = false; - if (currentColumn < leftColumn) { + bool need = !NeedDisplay.IsEmpty || wrapNeeded; + if (!wordWrap && currentColumn < leftColumn) { leftColumn = currentColumn; need = true; - } else if (currentColumn - leftColumn > Frame.Width + offB.width || - TextModel.DisplaySize (line, leftColumn, currentColumn).size >= Frame.Width + offB.width) { + } else if (!wordWrap && (currentColumn - leftColumn + RightOffset > Frame.Width + offB.width || + TextModel.DisplaySize (line, leftColumn, currentColumn).size + RightOffset >= Frame.Width + offB.width)) { leftColumn = Math.Max (TextModel.CalculateLeftColumn (line, leftColumn, - currentColumn, Frame.Width - 1 + offB.width, currentColumn), 0); + currentColumn + RightOffset, Frame.Width - 1 + offB.width, currentColumn), 0); need = true; } if (currentRow < topRow) { topRow = currentRow; need = true; - } else if (currentRow - topRow >= Frame.Height + offB.height) { - topRow = Math.Min (Math.Max (currentRow - Frame.Height + 1, 0), currentRow); + } else if (currentRow - topRow + BottomOffset >= Frame.Height + offB.height) { + topRow = Math.Min (Math.Max (currentRow - Frame.Height + 1 + BottomOffset, 0), currentRow); need = true; } if (need) { + if (wrapNeeded) { + WrapTextModel (); + wrapNeeded = false; + } SetNeedsDisplay (); } else { PositionCursor (); @@ -922,20 +1322,29 @@ namespace Terminal.Gui { } if (isRow) { topRow = Math.Max (idx > model.Count - 1 ? model.Count - 1 : idx, 0); - } else { - var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height); + } else if (!wordWrap) { + var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height + RightOffset); leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0); } SetNeedsDisplay (); } bool lastWasKill; + bool wrapNeeded; + bool shiftSelecting; /// public override bool ProcessKey (KeyEvent kb) { int restCount; List rest; + bool lineRemoved = false; + + if (shiftSelecting && selecting && !kb.Key.HasFlag (Key.ShiftMask) + && !kb.Key.HasFlag (Key.CtrlMask | Key.C)) { + shiftSelecting = false; + selecting = false; + } // Handle some state here - whether the last command was a kill // operation and the column tracking (up/down) @@ -958,6 +1367,10 @@ namespace Terminal.Gui { switch (kb.Key) { case Key.PageDown: case Key.V | Key.CtrlMask: + case Key.PageDown | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } int nPageDnShift = Frame.Height - 1; if (currentRow < model.Count) { if (columnTrack == -1) @@ -974,6 +1387,10 @@ namespace Terminal.Gui { case Key.PageUp: case ((int)'V' + Key.AltMask): + case Key.PageUp | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } int nPageUpShift = Frame.Height - 1; if (currentRow > 0) { if (columnTrack == -1) @@ -990,16 +1407,28 @@ namespace Terminal.Gui { case Key.N | Key.CtrlMask: case Key.CursorDown: + case Key.CursorDown | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } MoveDown (); break; case Key.P | Key.CtrlMask: case Key.CursorUp: + case Key.CursorUp | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } MoveUp (); break; case Key.F | Key.CtrlMask: case Key.CursorRight: + case Key.CursorRight | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } var currentLine = GetCurrentLine (); if (currentColumn < currentLine.Count) { currentColumn++; @@ -1009,6 +1438,7 @@ namespace Terminal.Gui { currentColumn = 0; if (currentRow >= topRow + Frame.Height) { topRow++; + SetNeedsDisplay (); } } } @@ -1017,6 +1447,10 @@ namespace Terminal.Gui { case Key.B | Key.CtrlMask: case Key.CursorLeft: + case Key.CursorLeft | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } if (currentColumn > 0) { currentColumn--; } else { @@ -1024,6 +1458,7 @@ namespace Terminal.Gui { currentRow--; if (currentRow < topRow) { topRow--; + SetNeedsDisplay (); } currentLine = GetCurrentLine (); currentColumn = currentLine.Count; @@ -1036,10 +1471,17 @@ namespace Terminal.Gui { case Key.Backspace: if (isReadOnly) break; + if (selecting) { + Cut (); + return true; + } if (currentColumn > 0) { // Delete backwards currentLine = GetCurrentLine (); currentLine.RemoveAt (currentColumn - 1); + if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn - 1)) { + wrapNeeded = true; + } currentColumn--; if (currentColumn < leftColumn) { leftColumn--; @@ -1055,15 +1497,26 @@ namespace Terminal.Gui { var prevCount = prevRow.Count; model.GetLine (prowIdx).AddRange (GetCurrentLine ()); model.RemoveLine (currentRow); + if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out lineRemoved, false)) { + wrapNeeded = true; + } currentRow--; - currentColumn = prevCount; + if (wrapNeeded && !lineRemoved) { + currentColumn = Math.Max (prevCount - 1, 0); + } else { + currentColumn = prevCount; + } Adjust (); } break; // Home, C-A case Key.Home: + case Key.Home | Key.ShiftMask: case Key.A | Key.CtrlMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } currentColumn = 0; Adjust (); break; @@ -1071,6 +1524,10 @@ namespace Terminal.Gui { case Key.D | Key.CtrlMask: // Delete if (isReadOnly) break; + if (selecting) { + Cut (); + return true; + } currentLine = GetCurrentLine (); if (currentColumn == currentLine.Count) { if (currentRow + 1 == model.Count) @@ -1078,17 +1535,27 @@ namespace Terminal.Gui { var nextLine = model.GetLine (currentRow + 1); currentLine.AddRange (nextLine); model.RemoveLine (currentRow + 1); + if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out _)) { + wrapNeeded = true; + } var sr = currentRow - topRow; SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1)); } else { currentLine.RemoveAt (currentColumn); + if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn)) { + wrapNeeded = true; + } var r = currentRow - topRow; SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1)); } break; case Key.End: + case Key.End | Key.ShiftMask: case Key.E | Key.CtrlMask: // End + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } currentLine = GetCurrentLine (); currentColumn = currentLine.Count; int pcol = leftColumn; @@ -1098,51 +1565,87 @@ namespace Terminal.Gui { case Key.K | Key.CtrlMask: // kill-to-end if (isReadOnly) break; + var cRow = currentRow; + SetWrapModel (); currentLine = GetCurrentLine (); + var setLastWasKill = true; if (currentLine.Count == 0) { model.RemoveLine (currentRow); - var val = ustring.Make ((Rune)'\n'); - if (lastWasKill) - AppendClipboard (val); - else - SetClipboard (val); + if (model.Count > 0 || lastWasKill) { + var val = ustring.Make ((Rune)'\n'); + if (lastWasKill) { + AppendClipboard (val); + } else { + SetClipboard (val); + } + } + if (model.Count == 0) { + // Prevents from adding line feeds if there is no more lines. + setLastWasKill = false; + } + if (currentRow > 0) { + currentRow--; + } } else { restCount = currentLine.Count - currentColumn; rest = currentLine.GetRange (currentColumn, restCount); - var val = StringFromRunes (rest); - if (lastWasKill) + var val = ustring.Empty; + if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { + val = ustring.Make ((Rune)'\n'); + } + val += StringFromRunes (rest); + if (lastWasKill) { AppendClipboard (val); - else + } else { SetClipboard (val); - currentLine.RemoveRange (currentColumn, restCount); + } + if (currentColumn == 0) { + model.RemoveLine (currentRow); + if (currentRow > 0) { + currentRow--; + } + } else { + currentLine.RemoveRange (currentColumn, restCount); + } + if (model.Count == 0) { + // Prevents from adding line feeds if there is no more lines. + setLastWasKill = false; + } } + UpdateWrapModel (); SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); - lastWasKill = true; + lastWasKill = setLastWasKill; break; case Key.Y | Key.CtrlMask: // Control-y, yank if (isReadOnly) break; - InsertText (Clipboard.Contents); - selecting = false; - break; + Paste (); + return true; case Key.Space | Key.CtrlMask: - selecting = true; + selecting = !selecting; selectionStartColumn = currentColumn; selectionStartRow = currentRow; break; + case ((int)'C' + Key.AltMask): + case Key.C | Key.CtrlMask: + Copy (); + return true; + case ((int)'W' + Key.AltMask): case Key.W | Key.CtrlMask: - SetClipboard (GetRegion ()); - if (!isReadOnly) - ClearRegion (); - selecting = false; - break; + case Key.X | Key.CtrlMask: + Cut (); + return true; case Key.CtrlMask | Key.CursorLeft: + case Key.CtrlMask | Key.CursorLeft | Key.ShiftMask: case (Key)((int)'B' + Key.AltMask): + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } var newPos = WordBackward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; @@ -1153,7 +1656,11 @@ namespace Terminal.Gui { break; case Key.CtrlMask | Key.CursorRight: + case Key.CtrlMask | Key.CursorRight | Key.ShiftMask: case (Key)((int)'F' + Key.AltMask): + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } newPos = WordForward (currentColumn, currentRow); if (newPos.HasValue) { currentColumn = newPos.Value.col; @@ -1170,6 +1677,10 @@ namespace Terminal.Gui { rest = currentLine.GetRange (currentColumn, restCount); currentLine.RemoveRange (currentColumn, restCount); model.AddLine (currentRow + 1, rest); + if (wordWrap) { + wrapManager.AddLine (currentRow, currentColumn); + wrapNeeded = true; + } currentRow++; bool fullNeedsDisplay = false; if (currentRow >= topRow + Frame.Height) { @@ -1177,7 +1688,7 @@ namespace Terminal.Gui { fullNeedsDisplay = true; } currentColumn = 0; - if (currentColumn < leftColumn) { + if (!wordWrap && currentColumn < leftColumn) { fullNeedsDisplay = true; leftColumn = 0; } @@ -1189,10 +1700,18 @@ namespace Terminal.Gui { break; case Key.CtrlMask | Key.End: + case Key.CtrlMask | Key.End | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } MoveEnd (); break; case Key.CtrlMask | Key.Home: + case Key.CtrlMask | Key.Home | Key.ShiftMask: + if (kb.Key.HasFlag (Key.ShiftMask)) { + StartSelecting (); + } MoveHome (); break; @@ -1203,16 +1722,96 @@ namespace Terminal.Gui { //So that special keys like tab can be processed if (isReadOnly) return true; + if (selecting) { + Cut (); + } Insert ((uint)kb.Key); currentColumn++; if (currentColumn >= leftColumn + Frame.Width) { leftColumn++; SetNeedsDisplay (); } - PositionCursor (); + break; + } + DoNeededAction (); + + return true; + } + + /// + public override bool OnKeyUp (KeyEvent kb) + { + switch (kb.Key) { + case Key.Space | Key.CtrlMask: return true; } - return true; + + return false; + } + + void DoNeededAction () + { + if (NeedDisplay.IsEmpty) { + PositionCursor (); + } else { + Adjust (); + } + } + + /// + /// Copy the selected text to the clipboard contents. + /// + public void Copy () + { + SetWrapModel (); + SetClipboard (GetRegion ()); + UpdateWrapModel (); + DoNeededAction (); + } + + /// + /// Cut the selected text to the clipboard contents. + /// + public void Cut () + { + SetWrapModel (); + SetClipboard (GetRegion ()); + if (!isReadOnly) { + ClearRegion (); + } + UpdateWrapModel (); + selecting = false; + DoNeededAction (); + } + + /// + /// Paste the clipboard contents into the current selected position. + /// + public void Paste () + { + if (isReadOnly) { + return; + } + + SetWrapModel (); + if (selecting) { + ClearRegion (); + } + InsertText (Clipboard.Contents); + UpdateWrapModel (); + selecting = false; + DoNeededAction (); + } + + void StartSelecting () + { + if (shiftSelecting && selecting) { + return; + } + shiftSelecting = true; + selecting = true; + selectionStartColumn = currentColumn; + selectionStartRow = currentRow; } void MoveUp () @@ -1238,7 +1837,7 @@ namespace Terminal.Gui { columnTrack = currentColumn; } currentRow++; - if (currentRow >= topRow + Frame.Height) { + if (currentRow + BottomOffset >= topRow + Frame.Height) { topRow++; SetNeedsDisplay (); } @@ -1269,7 +1868,11 @@ namespace Terminal.Gui { } } - Rune RuneAt (int col, int row) => model.GetLine (row) [col]; + Rune RuneAt (int col, int row) + { + var line = model.GetLine (row); + return line [col > line.Count - 1 ? line.Count - 1 : col]; + } /// /// Will scroll the to the last line and position the cursor there. @@ -1277,6 +1880,8 @@ namespace Terminal.Gui { public void MoveEnd () { currentRow = model.Count - 1; + var line = GetCurrentLine (); + currentColumn = line.Count; TrackColumn (); PositionCursor (); } @@ -1287,6 +1892,7 @@ namespace Terminal.Gui { public void MoveHome () { currentRow = 0; + currentColumn = 0; TrackColumn (); PositionCursor (); } @@ -1351,6 +1957,9 @@ namespace Terminal.Gui { if (Rune.IsLetterOrDigit (rune)) break; } + if (row != fromRow && Rune.IsLetterOrDigit (rune)) { + return (col, row); + } while (MoveNext (ref col, ref row, out rune)) { if (!Rune.IsLetterOrDigit (rune)) break; @@ -1362,7 +1971,7 @@ namespace Terminal.Gui { } } if (fromCol != col || fromRow != row) - return (col, row); + return (col + 1, row); return null; } catch (Exception) { return null; @@ -1374,7 +1983,7 @@ namespace Terminal.Gui { if (fromRow == 0 && fromCol == 0) return null; - var col = fromCol; + var col = Math.Max (fromCol - 1, 0); var row = fromRow; try { var rune = RuneAt (col, row); @@ -1384,9 +1993,18 @@ namespace Terminal.Gui { if (Rune.IsLetterOrDigit (rune)) break; } + int lastValidCol = -1; while (MovePrev (ref col, ref row, out rune)) { - if (!Rune.IsLetterOrDigit (rune)) + if (col == 0 && Rune.IsLetterOrDigit (rune)) { + return (col, row); + } else if (col == 0 && !Rune.IsLetterOrDigit (rune) && lastValidCol > -1) { + col = lastValidCol; + return (col, row); + } + if (!Rune.IsLetterOrDigit (rune)) { break; + } + lastValidCol = Rune.IsLetterOrDigit (rune) ? col : -1; } } else { while (MovePrev (ref col, ref row, out rune)) { @@ -1394,8 +2012,11 @@ namespace Terminal.Gui { break; } } - if (fromCol != col || fromRow != row) - return (col, row); + if (fromCol != col && fromRow == row) { + return (col == 0 ? col : col + 1, row); + } else if (fromCol != col && fromRow != row) { + return (col + 1, row); + } return null; } catch (Exception) { return null; @@ -1405,8 +2026,11 @@ namespace Terminal.Gui { /// public override bool MouseEvent (MouseEvent ev) { - if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && - !ev.Flags.HasFlag (MouseFlags.WheeledDown) && !ev.Flags.HasFlag (MouseFlags.WheeledUp)) { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed) + && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) + && !ev.Flags.HasFlag (MouseFlags.Button1Released) + && !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift) + && !ev.Flags.HasFlag (MouseFlags.WheeledDown) && !ev.Flags.HasFlag (MouseFlags.WheeledUp)) { return false; } @@ -1419,21 +2043,11 @@ namespace Terminal.Gui { } if (ev.Flags == MouseFlags.Button1Clicked) { - if (model.Count > 0) { - var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0); - if (ev.Y > maxCursorPositionableLine) { - currentRow = maxCursorPositionableLine; - } else { - currentRow = ev.Y + topRow; - } - var r = GetCurrentLine (); - var idx = TextModel.GetColFromX (r, leftColumn, ev.X); - if (idx - leftColumn >= r.Count) { - currentColumn = r.Count - leftColumn; - } else { - currentColumn = idx + leftColumn; - } + if (shiftSelecting) { + shiftSelecting = false; + selecting = false; } + ProcessMouseClick (ev, out _); PositionCursor (); lastWasKill = false; columnTrack = currentColumn; @@ -1453,10 +2067,94 @@ namespace Terminal.Gui { lastWasKill = false; columnTrack = currentColumn; ScrollTo (leftColumn - 1, false); + } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + ProcessMouseClick (ev, out List line); + PositionCursor (); + if (model.Count > 0 && shiftSelecting && selecting) { + if (currentRow - topRow + BottomOffset >= Frame.Height - 1 + && model.Count + BottomOffset > topRow + currentRow) { + ScrollTo (topRow + Frame.Height); + } else if (topRow > 0 && currentRow <= topRow) { + ScrollTo (topRow - Frame.Height); + } else if (ev.Y >= Frame.Height) { + ScrollTo (model.Count + BottomOffset); + } else if (ev.Y < 0 && topRow > 0) { + ScrollTo (0); + } + if (currentColumn - leftColumn + RightOffset >= Frame.Width - 1 + && line.Count + RightOffset > leftColumn + currentColumn) { + ScrollTo (leftColumn + Frame.Width, false); + } else if (leftColumn > 0 && currentColumn <= leftColumn) { + ScrollTo (leftColumn - Frame.Width, false); + } else if (ev.X >= Frame.Width) { + ScrollTo (line.Count + RightOffset, false); + } else if (ev.X < 0 && leftColumn > 0) { + ScrollTo (0, false); + } + } + lastWasKill = false; + columnTrack = currentColumn; + } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)) { + if (!shiftSelecting) { + StartSelecting (); + } + ProcessMouseClick (ev, out _); + PositionCursor (); + lastWasKill = false; + columnTrack = currentColumn; + } else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) { + if (shiftSelecting) { + shiftSelecting = false; + selecting = false; + } + ProcessMouseClick (ev, out _); + PositionCursor (); + if (!selecting) { + StartSelecting (); + } + lastWasKill = false; + columnTrack = currentColumn; + if (Application.mouseGrabView == null) { + Application.GrabMouse (this); + } + } else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) { + Application.UngrabMouse (); } return true; } + + void ProcessMouseClick (MouseEvent ev, out List line) + { + List r = null; + if (model.Count > 0) { + var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0); + if (Math.Max (ev.Y, 0) > maxCursorPositionableLine) { + currentRow = maxCursorPositionableLine + topRow; + } else { + currentRow = Math.Max (ev.Y + topRow, 0); + } + r = GetCurrentLine (); + var idx = TextModel.GetColFromX (r, leftColumn, Math.Max (ev.X, 0)); + if (idx - leftColumn >= r.Count + RightOffset) { + currentColumn = Math.Max (r.Count - leftColumn + RightOffset, 0); + } else { + currentColumn = idx + leftColumn; + } + } + + line = r; + } + + /// + public override bool OnLeave (View view) + { + if (Application.mouseGrabView != null && Application.mouseGrabView == this) { + Application.UngrabMouse (); + } + + return base.OnLeave (view); + } } } diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index c3e4ffc5c..36c7b0ce1 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -14,6 +14,7 @@ namespace UICatalog { private TextView _textView; private bool _saved = true; private ScrollBarView _scrollBar; + private byte [] _originalText; public override void Init (Toplevel top, ColorScheme colorScheme) { @@ -28,13 +29,14 @@ namespace UICatalog { new MenuItem ("_New", "", () => New()), new MenuItem ("_Open", "", () => Open()), new MenuItem ("_Save", "", () => Save()), + new MenuItem ("_Save As", "", () => SaveAs()), null, new MenuItem ("_Quit", "", () => Quit()), }), new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", () => Copy()), - new MenuItem ("C_ut", "", () => Cut()), - new MenuItem ("_Paste", "", () => Paste()) + new MenuItem ("_Copy", "", () => Copy(),null,null, Key.CtrlMask | Key.C), + new MenuItem ("C_ut", "", () => Cut(),null,null, Key.CtrlMask | Key.W), + new MenuItem ("_Paste", "", () => Paste(),null,null, Key.CtrlMask | Key.Y) }), new MenuBarItem ("_ScrollBarView", CreateKeepChecked ()), new MenuBarItem ("_Cursor", new MenuItem [] { @@ -49,7 +51,8 @@ namespace UICatalog { new MenuItem (" V_ertical Fix", "", () => SetCursor(CursorVisibility.VerticalFix)), new MenuItem (" B_ox Fix", "", () => SetCursor(CursorVisibility.BoxFix)), new MenuItem (" U_nderline Fix","", () => SetCursor(CursorVisibility.UnderlineFix)) - }) + }), + new MenuBarItem ("Forma_t", CreateWrapChecked ()) }); Top.Add (menu); @@ -76,7 +79,8 @@ namespace UICatalog { Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), - + BottomOffset = 1, + RightOffset = 1 }; LoadFile (); @@ -102,10 +106,12 @@ namespace UICatalog { }; _textView.DrawContent += (e) => { - _scrollBar.Size = _textView.Lines - 1; + _scrollBar.Size = _textView.Lines; _scrollBar.Position = _textView.TopRow; - _scrollBar.OtherScrollBarView.Size = _textView.Maxlength; - _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn; + if (_scrollBar.OtherScrollBarView != null) { + _scrollBar.OtherScrollBarView.Size = _textView.Maxlength; + _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn; + } _scrollBar.LayoutSubviews (); _scrollBar.Refresh (); }; @@ -117,20 +123,23 @@ namespace UICatalog { private void New () { - Win.Title = _fileName = "Untitled"; - throw new NotImplementedException (); + if (!CanCloseFile ()) { + return; + } + + Win.Title = "Untitled.txt"; + _fileName = null; + _originalText = new System.IO.MemoryStream ().ToArray (); + _textView.Text = _originalText; } private void LoadFile () { - if (!_saved) { - MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); - } - if (_fileName != null) { // BUGBUG: #452 TextView.LoadFile keeps file open and provides no way of closing it //_textView.LoadFile(_fileName); _textView.Text = System.IO.File.ReadAllText (_fileName); + _originalText = _textView.Text.ToByteArray (); Win.Title = _fileName; _saved = true; } @@ -138,20 +147,23 @@ namespace UICatalog { private void Paste () { - MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); + if (_textView != null) { + _textView.Paste (); + } } private void Cut () { - MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); + if (_textView != null) { + _textView.Cut (); + } } private void Copy () { - MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); - //if (_textView != null && _textView.SelectedLength != 0) { - // _textView.Copy (); - //} + if (_textView != null) { + _textView.Copy (); + } } private void SetCursor (CursorVisibility visibility) @@ -159,8 +171,29 @@ namespace UICatalog { _textView.DesiredCursorVisibility = visibility; } + private bool CanCloseFile () + { + if (_textView.Text == _originalText) { + return true; + } + + var r = MessageBox.ErrorQuery ("Save File", + $"Do you want save changes in {Win.Title}?", "Yes", "No", "Cancel"); + if (r == 0) { + return Save (); + } else if (r == 1) { + return true; + } + + return false; + } + private void Open () { + if (!CanCloseFile ()) { + return; + } + var d = new OpenDialog ("Open", "Open a file") { AllowsMultipleSelection = false }; Application.Run (d); @@ -170,22 +203,62 @@ namespace UICatalog { } } - private void Save () + private bool Save () { if (_fileName != null) { // BUGBUG: #279 TextView does not know how to deal with \r\n, only \r // As a result files saved on Windows and then read back will show invalid chars. - System.IO.File.WriteAllText (_fileName, _textView.Text.ToString()); - _saved = true; + return SaveFile (Win.Title.ToString (), _fileName); + } else { + return SaveAs (); } } + private bool SaveAs () + { + var sd = new SaveDialog ("Save file", "Choose the path where to save the file."); + sd.FilePath = System.IO.Path.Combine (sd.FilePath.ToString (), Win.Title.ToString ()); + Application.Run (sd); + + if (!sd.Canceled) { + if (System.IO.File.Exists (sd.FilePath.ToString ())) { + if (MessageBox.Query ("Save File", + "File already exists. Overwrite any way?", "No", "Ok") == 1) { + return SaveFile (sd.FileName.ToString (), sd.FilePath.ToString ()); + } else { + return _saved = false; + } + } else { + return SaveFile (sd.FileName.ToString (), sd.FilePath.ToString ()); + } + } else { + return _saved = false; + } + } + + private bool SaveFile (string title, string file) + { + try { + Win.Title = title; + _fileName = file; + System.IO.File.WriteAllText (_fileName, _textView.Text.ToString ()); + _saved = true; + MessageBox.Query ("Save File", "File was successfully saved.", "Ok"); + + } catch (Exception ex) { + MessageBox.ErrorQuery ("Error", ex.Message, "Ok"); + return false; + } + + return true; + } + private void Quit () { Application.RequestStop (); } - private void CreateDemoFile(string fileName) + private void CreateDemoFile (string fileName) { var sb = new StringBuilder (); // BUGBUG: #279 TextView does not know how to deal with \r\n, only \r @@ -211,6 +284,27 @@ namespace UICatalog { return new MenuItem [] { item }; } + private MenuItem [] CreateWrapChecked () + { + var item = new MenuItem (); + item.Title = "Word Wrap"; + item.CheckType |= MenuItemCheckStyle.Checked; + item.Checked = false; + item.Action += () => { + _textView.WordWrap = item.Checked = !item.Checked; + if (_textView.WordWrap) { + _scrollBar.AutoHideScrollBars = false; + _scrollBar.OtherScrollBarView.ShowScrollIndicator = false; + _textView.BottomOffset = 0; + } else { + _scrollBar.AutoHideScrollBars = true; + _textView.BottomOffset = 1; + } + }; + + return new MenuItem [] { item }; + } + public override void Run () { base.Run ();