From e58c15f2e68b51333c323412d9ff6f073f4ca16b Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 7 Oct 2024 00:19:51 +0100 Subject: [PATCH] Add undo and redo support for cells attribute. --- Terminal.Gui/Drawing/Cell.cs | 25 +- Terminal.Gui/Drawing/ColorScheme.Colors.cs | 119 +++++ Terminal.Gui/Resources/Strings.Designer.cs | 9 + Terminal.Gui/Resources/Strings.fr-FR.resx | 27 +- Terminal.Gui/Resources/Strings.ja-JP.resx | 137 ++--- Terminal.Gui/Resources/Strings.pt-PT.resx | 29 +- Terminal.Gui/Resources/Strings.resx | 525 ++++++++++---------- Terminal.Gui/Resources/Strings.zh-Hans.resx | 15 +- Terminal.Gui/Views/TextField.cs | 4 +- Terminal.Gui/Views/TextView.cs | 228 +++++++-- UICatalog/Scenarios/Editor.cs | 181 +------ UnitTests/Views/TextViewTests.cs | 80 +++ 12 files changed, 804 insertions(+), 575 deletions(-) diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 581cc6bd6..16af046a7 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -72,8 +72,8 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru public static List> StringToLinesOfCells (string content, Attribute? attribute = null) { List cells = content.EnumerateRunes () - .Select (x => new Cell { Rune = x, Attribute = attribute }) - .ToList (); + .Select (x => new Cell { Rune = x, Attribute = attribute }) + .ToList (); return SplitNewLines (cells); } @@ -93,6 +93,27 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru return str; } + /// Converts a generic collection into a string. + /// The enumerable cell to convert. + /// + public static string ToString (List> cellsList) + { + var str = string.Empty; + + for (var i = 0; i < cellsList.Count; i++) + { + IEnumerable cellList = cellsList [i]; + str += ToString (cellList); + + if (i + 1 < cellsList.Count) + { + str += Environment.NewLine; + } + } + + return str; + } + // Turns the string into cells, this does not split the contents on a newline if it is present. internal static List StringToCells (string str, Attribute? attribute = null) diff --git a/Terminal.Gui/Drawing/ColorScheme.Colors.cs b/Terminal.Gui/Drawing/ColorScheme.Colors.cs index ca14a4986..a7c827be5 100644 --- a/Terminal.Gui/Drawing/ColorScheme.Colors.cs +++ b/Terminal.Gui/Drawing/ColorScheme.Colors.cs @@ -173,4 +173,123 @@ public sealed class Colors : INotifyCollectionChanged, IDictionary + /// Open a with two or , based on the + /// is false or true, respectively, for + /// and colors. + /// + /// The title to show in the dialog. + /// The current attribute used. + /// The new attribute. + /// if a new color was accepted, otherwise . + public static bool PromptForColors (string title, Attribute? currentAttribute, out Attribute newAttribute) + { + var accept = false; + + var d = new Dialog + { + Title = title, + Width = Application.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), + Height = 20 + }; + + var btnOk = new Button + { + X = Pos.Center () - 5, + Y = Application.Force16Colors ? 6 : 4, + Text = "Ok", + Width = Dim.Auto (), + IsDefault = true + }; + + btnOk.Accept += (s, e) => + { + accept = true; + e.Handled = true; + Application.RequestStop (); + }; + + var btnCancel = new Button + { + X = Pos.Center () + 5, + Y = 4, + Text = "Cancel", + Width = Dim.Auto () + }; + + btnCancel.Accept += (s, e) => + { + e.Handled = true; + Application.RequestStop (); + }; + + d.Add (btnOk); + d.Add (btnCancel); + + d.AddButton (btnOk); + d.AddButton (btnCancel); + + View cpForeground; + + if (Application.Force16Colors) + { + cpForeground = new ColorPicker16 + { + SelectedColor = currentAttribute!.Value.Foreground.GetClosestNamedColor16 (), + Width = Dim.Fill (), + BorderStyle = LineStyle.Single, + Title = "Foreground" + }; + } + else + { + cpForeground = new ColorPicker + { + SelectedColor = currentAttribute!.Value.Foreground, + Width = Dim.Fill (), + Style = new () { ShowColorName = true, ShowTextFields = true }, + BorderStyle = LineStyle.Single, + Title = "Foreground" + }; + ((ColorPicker)cpForeground).ApplyStyleChanges (); + } + + View cpBackground; + + if (Application.Force16Colors) + { + cpBackground = new ColorPicker16 + { + SelectedColor = currentAttribute!.Value.Background.GetClosestNamedColor16 (), + Y = Pos.Bottom (cpForeground) + 1, + Width = Dim.Fill (), + BorderStyle = LineStyle.Single, + Title = "Background" + }; + } + else + { + cpBackground = new ColorPicker + { + SelectedColor = currentAttribute!.Value.Background, + Width = Dim.Fill (), + Y = Pos.Bottom (cpForeground) + 1, + Style = new () { ShowColorName = true, ShowTextFields = true }, + BorderStyle = LineStyle.Single, + Title = "Background" + }; + ((ColorPicker)cpBackground).ApplyStyleChanges (); + } + + d.Add (cpForeground, cpBackground); + + Application.Run (d); + d.Dispose (); + Color newForeColor = Application.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor; + Color newBackColor = Application.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor; + newAttribute = new (newForeColor, newBackColor); + + return accept; + } } diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index 491473014..2befd2737 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -1437,6 +1437,15 @@ namespace Terminal.Gui.Resources { } } + /// + /// Looks up a localized string similar to Co_lors. + /// + internal static string ctxColors { + get { + return ResourceManager.GetString("ctxColors", resourceCulture); + } + } + /// /// Looks up a localized string similar to _Copy. /// diff --git a/Terminal.Gui/Resources/Strings.fr-FR.resx b/Terminal.Gui/Resources/Strings.fr-FR.resx index 746c45499..c20959da4 100644 --- a/Terminal.Gui/Resources/Strings.fr-FR.resx +++ b/Terminal.Gui/Resources/Strings.fr-FR.resx @@ -117,27 +117,27 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Tout _sélectionner + + + _Tout supprimer + _Copier Co_uper - - _Tout supprimer - C_oller - - _Rétablir - - - Tout _sélectionner - _Annuler + + _Rétablir + _Dossier @@ -168,16 +168,19 @@ Prochai_n... + + Ouvrir + Enregistrer E_nregistrer sous - - Ouvrir - Sélecteur de Date + + Cou_leurs + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.ja-JP.resx b/Terminal.Gui/Resources/Strings.ja-JP.resx index 4a825c51c..fa4bda421 100644 --- a/Terminal.Gui/Resources/Strings.ja-JP.resx +++ b/Terminal.Gui/Resources/Strings.ja-JP.resx @@ -117,27 +117,27 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 全て選択 (_S) + + + 全て削除 (_D) + コピー (_C) 切り取り (_T) - - 全て削除 (_D) - 貼り付け (_P) - - やり直し (_R) - - - 全て選択 (_S) - 元に戻す (_U) + + やり直し (_R) + ディレクトリ @@ -171,80 +171,74 @@ 同じ名前のディレクトリはすでに存在しました - - “{0}”を削除もよろしいですか?この操作は元に戻りません - - - タイプ - - - サイズ - - - パスを入力 - - - ファイル名 - - - 新規ディレクトリ - - - いいえ (_N) - - - はい (_Y) - - - 変更日時 - - - すでに存在したファイルまたはディレクトリを選択してください - すでに存在したディレクトリを選択してください - - すでに存在したファイルを選択してください - - - 名前: - - - {0} を削除 - - - 新規失敗 - - - 既存 - - - 名前を変更 - - - 変更失敗 - - - 削除失敗 - 同じ名前のファイルはすでに存在しました + + すでに存在したファイルを選択してください + + + ファイル名 + + + すでに存在したファイルまたはディレクトリを選択してください + + + 変更日時 + + + パスを入力 + 検索を入力 + + サイズ + + + タイプ + ファイルタイプが間違っでいます 任意ファイル - - キャンセル (_C) + + “{0}”を削除もよろしいですか?この操作は元に戻りません - - OK (_O) + + 削除失敗 + + + {0} を削除 + + + 新規失敗 + + + 新規ディレクトリ + + + いいえ (_N) + + + 変更失敗 + + + 名前: + + + 名前を変更 + + + はい (_Y) + + + 既存 開く (_O) @@ -255,6 +249,12 @@ 名前を付けて保存 (_S) + + OK (_O) + + + キャンセル (_C) + 削除 (_D) @@ -276,4 +276,7 @@ 日付ピッカー + + 絵の具 (_L) + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.pt-PT.resx b/Terminal.Gui/Resources/Strings.pt-PT.resx index 2cffa6cd7..28aabf522 100644 --- a/Terminal.Gui/Resources/Strings.pt-PT.resx +++ b/Terminal.Gui/Resources/Strings.pt-PT.resx @@ -117,27 +117,27 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + _Selecionar Tudo + + + _Apagar Tudo + _Copiar Cor_tar - - _Apagar Tudo - Co_lar - - _Refazer - - - _Selecionar Tudo - _Desfazer + + _Refazer + Diretório @@ -168,16 +168,19 @@ S_eguir... - - Guardar como + + Abrir Guardar - - Abrir + + Guardar como Seletor de Data + + Co_res + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index 8380fb408..9333d7157 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -1,6 +1,6 @@  - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + _Select All - + _Delete All - + _Copy - + Cu_t - + _Paste - + _Undo - + _Redo - + Directory - + File - + Save - + Save as - + Open - + Select folder - + Select Mixed - + _Back - + Fi_nish - + _Next... - + Directory already exists with that name When trying to save a file with a name already taken by a directory - + Must select an existing directory - + File already exists with that name - + Must select an existing file When trying to save a directory with a name already used by a file - + Filename - + Must select an existing file or directory - + Modified - + Enter Path - + Enter Search - + Size - + Type - + Wrong file type When trying to open/save a file that does not match the provided filter (e.g. csv) - + Any Files Describes an AllowedType that matches anything - + Are you sure you want to delete '{0}'? This operation is permanent - + Delete Failed - + Delete {0} - + New Failed - + New Folder - + _No - + Rename Failed - + Name: - + Rename - + _Yes - + Existing - + O_pen - + _Save - + Save _as - + _OK - + _Cancel - + _Delete - + _Hide {0} - + _New - + _Rename - + _Sort {0} ASC - + _Sort {0} DESC - + Date Picker - + AliceBlue - + AntiqueWhite - + Aquamarine - + Azure - + Beige - + Bisque - + Black - + BlanchedAlmond - + Blue - + BlueViolet - + Brown - + BurlyWood - + CadetBlue - + Chartreuse - + Chocolate - + Coral - + CornflowerBlue - + Cornsilk - + Crimson - + Cyan - + DarkBlue - + DarkCyan - + DarkGoldenRod - + DarkGrey - + DarkGreen - + DarkKhaki - + DarkMagenta - + DarkOliveGreen - + DarkOrange - + DarkOrchid - + DarkRed - + DarkSalmon - + DarkSeaGreen - + DarkSlateBlue - + DarkSlateGrey - + DarkTurquoise - + DarkViolet - + DeepPink - + DeepSkyBlue - + DimGray - + DodgerBlue - + FireBrick - + FloralWhite - + ForestGreen - + Gainsboro - + GhostWhite - + Gold - + GoldenRod - + Gray - + Green - + GreenYellow - + HoneyDew - + HotPink - + IndianRed - + Indigo - + Ivory - + Khaki - + Lavender - + LavenderBlush - + LawnGreen - + LemonChiffon - + LightBlue - + LightCoral - + LightCyan - + LightGoldenRodYellow - + LightGray - + LightGreen - + LightPink - + LightSalmon - + LightSeaGreen - + LightSkyBlue - + LightSlateGrey - + LightSteelBlue - + LightYellow - + Lime - + LimeGreen - + Linen - + Magenta - + Maroon - + MediumAquaMarine - + MediumBlue - + MediumOrchid - + MediumPurple - + MediumSeaGreen - + MediumSlateBlue - + MediumSpringGreen - + MediumTurquoise - + MediumVioletRed - + MidnightBlue - + MintCream - + MistyRose - + Moccasin - + NavajoWhite - + Navy - + OldLace - + Olive - + OliveDrab - + Orange - + OrangeRed - + Orchid - + PaleGoldenRod - + PaleGreen - + PaleTurquoise - + PaleVioletRed - + PapayaWhip - + PeachPuff - + Peru - + Pink - + Plum - + PowderBlue - + Purple - + RebeccaPurple - + Red - + RosyBrown - + RoyalBlue - + SaddleBrown - + Salmon - + SandyBrown - + SeaGreen - + SeaShell - + Sienna - + Silver - + SkyBlue - + SlateBlue - + SlateGray - + Snow - + SpringGreen - + SteelBlue - + Tan - + Teal - + Thistle - + Tomato - + Turquoise - + Violet - + Wheat - + White - + WhiteSmoke - + Yellow - + YellowGreen - + BrightBlue - + BrightCyan - + BrightRed - - + + BrightGreen - - + + BrightMagenta - - + + BrightYellow - - + + DarkGray - + + + Co_lors + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.zh-Hans.resx b/Terminal.Gui/Resources/Strings.zh-Hans.resx index 009fdd479..8ea63e91d 100644 --- a/Terminal.Gui/Resources/Strings.zh-Hans.resx +++ b/Terminal.Gui/Resources/Strings.zh-Hans.resx @@ -153,9 +153,6 @@ 打开 - - 下一步 (_N)... - 选择文件夹 (_S) @@ -168,6 +165,9 @@ 结束 (_N) + + 下一步 (_N)... + 已存在相同名称的目录 @@ -240,9 +240,6 @@ 已有 - - 确定 (_O) - 打开 (_O) @@ -252,6 +249,9 @@ 另存为 (_S) + + 确定 (_O) + 取消 (_C) @@ -276,4 +276,7 @@ 日期选择器 + + 旗帜 (_L) + \ No newline at end of file diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index a352e586c..3862a4acb 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -464,7 +464,7 @@ public class TextField : View /// Indicates whatever the text was changed or not. if the text was changed /// otherwise. /// - public bool IsDirty => _historyText.IsDirty (Text); + public bool IsDirty => _historyText.IsDirty ([Cell.StringToCells (Text)]); /// If set to true its not allow any changes in the text. public bool ReadOnly { get; set; } @@ -594,7 +594,7 @@ public class TextField : View } /// Allows clearing the items updating the original text. - public void ClearHistoryChanges () { _historyText.Clear (Text); } + public void ClearHistoryChanges () { _historyText.Clear ([Cell.StringToCells (Text)]); } /// Copy the selected text to the clipboard. public virtual void Copy () diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 3eda736a5..f283f47d3 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -180,7 +180,7 @@ internal class TextModel { if (_lines.Count > 0 && pos < _lines.Count) { - _lines [pos] = new (runes); + _lines [pos] = [..runes]; } else if (_lines.Count == 0 || (_lines.Count > 0 && pos >= _lines.Count)) { @@ -1125,12 +1125,13 @@ internal partial class HistoryText Original, Replaced, Removed, - Added + Added, + Attribute } - private readonly List _historyTextItems = new (); + private readonly List _historyTextItems = []; private int _idxHistoryText = -1; - private string? _originalText; + private List> _originalCellsList = []; public bool HasHistoryChanges => _idxHistoryText > -1; public bool IsFromHistory { get; private set; } @@ -1165,15 +1166,51 @@ internal partial class HistoryText public event EventHandler? ChangeText; - public void Clear (string text) + public void Clear (List> cellsList) { _historyTextItems.Clear (); _idxHistoryText = -1; - _originalText = text; + _originalCellsList.Clear (); + + foreach (List cells in cellsList) + { + _originalCellsList.Add ([..cells]); + } + OnChangeText (null); } - public bool IsDirty (string text) { return _originalText != text; } + public bool IsDirty (List> cellsList) + { + if (cellsList.Count != _originalCellsList.Count) + { + return true; + } + + for (var r = 0; r < cellsList.Count; r++) + { + List cells = cellsList [r]; + List originalCells = _originalCellsList [r]; + + if (cells.Count != originalCells.Count) + { + return true; + } + + for (var c = 0; c < cells.Count; c++) + { + Cell cell = cells [c]; + Cell originalCell = originalCells [c]; + + if (!cell.Equals (originalCell)) + { + return true; + } + } + } + + return false; + } public void Redo () { @@ -1227,7 +1264,8 @@ internal partial class HistoryText if (_idxHistoryText - 1 > -1 && (_historyTextItems [_idxHistoryText - 1].LineStatus == LineStatus.Added || _historyTextItems [_idxHistoryText - 1].LineStatus == LineStatus.Removed - || (historyTextItem.LineStatus == LineStatus.Replaced && _historyTextItems [_idxHistoryText - 1].LineStatus == LineStatus.Original))) + || (historyTextItem.LineStatus == LineStatus.Replaced && _historyTextItems [_idxHistoryText - 1].LineStatus == LineStatus.Original) + || (historyTextItem.LineStatus == LineStatus.Attribute && _historyTextItems [_idxHistoryText - 1].LineStatus == LineStatus.Original))) { _idxHistoryText--; @@ -1858,7 +1896,7 @@ public class TextView : View CursorVisibility = CursorVisibility.Default; Used = true; - // By default, disable hotkeys (in case someome sets Title) + // By default, disable hotkeys (in case someone sets Title) HotKeySpecifier = new ('\xffff'); _model.LinesLoaded += Model_LinesLoaded!; @@ -2264,6 +2302,15 @@ public class TextView : View } ); + AddCommand ( + Command.Open, + () => + { + PromptForColors (); + + return true; + }); + // Default keybindings for this view KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.V.WithCtrl, Command.PageDown); @@ -2357,6 +2404,8 @@ public class TextView : View KeyBindings.Add (Key.G.WithCtrl, Command.DeleteAll); KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll); + KeyBindings.Add (Key.L.WithCtrl, Command.Open); + _currentCulture = Thread.CurrentThread.CurrentUICulture; ContextMenu = new (); @@ -2495,8 +2544,8 @@ public class TextView : View /// public bool IsDirty { - get => _historyText.IsDirty (Text); - set => _historyText.Clear (Text); + get => _historyText.IsDirty (_model.GetAllLines ()); + set => _historyText.Clear (_model.GetAllLines ()); } /// Gets or sets the left column. @@ -2585,15 +2634,21 @@ public class TextView : View /// Length of the selected text. public int SelectedLength => GetSelectedLength (); - private List> _selectedCellsList = []; - /// /// Gets the selected text as /// /// List{List{Cell}} /// /// - public List> SelectedCellsList => _selectedCellsList; + public List> SelectedCellsList + { + get + { + GetRegion (out List> selectedCellsList); + + return selectedCellsList; + } + } /// The selected text. public string SelectedText @@ -2689,7 +2744,7 @@ public class TextView : View OnTextChanged (); SetNeedsDisplay (); - _historyText.Clear (Text); + _historyText.Clear (_model.GetAllLines ()); } } @@ -2741,7 +2796,7 @@ public class TextView : View /// Allows clearing the items updating the original text. - public void ClearHistoryChanges () { _historyText?.Clear (Text); } + public void ClearHistoryChanges () { _historyText?.Clear (_model.GetAllLines ()); } /// Closes the contents of the stream into the . /// true, if stream was closed, false otherwise. @@ -2763,7 +2818,100 @@ public class TextView : View /// public event EventHandler? ContentsChanged; - private string _copiedText; + 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 (); + Selecting = false; + + _historyText.Add ( + [.. selectedCellsOriginal], + new (startCol, startRow) + ); + + _historyText.Add ( + [.. selectedCellsChanged], + new (startCol, startRow), + HistoryText.LineStatus.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 new (ColorScheme!.Focus); + } + + line = GetCurrentLine (); + + if (line [Math.Min (CurrentColumn, line.Count - 1)].Attribute is { } attribute) + { + return new (attribute); + } + + return new (ColorScheme!.Focus); + } + + /// + /// Open a dialog to set the foreground and background colors. + /// + public void PromptForColors () + { + if (!Colors.PromptForColors ( + "Colors", + GetSelectedCellAttribute (), + out Attribute newAttribute + )) + { + return; + } + + var attribute = new Attribute ( + newAttribute.Foreground, + newAttribute.Background + ); + + ApplyCellsAttribute (attribute); + } + + private string? _copiedText; private List> _copiedCellsList = []; /// Copy the selected text to the clipboard contents. @@ -3070,7 +3218,7 @@ public class TextView : View { SetWrapModel (); res = _model.LoadFile (path); - _historyText.Clear (Text); + _historyText.Clear (_model.GetAllLines ()); ResetPosition (); } finally @@ -3092,7 +3240,7 @@ public class TextView : View { SetWrapModel (); _model.LoadStream (stream); - _historyText.Clear (Text); + _historyText.Clear (_model.GetAllLines ()); ResetPosition (); SetNeedsDisplay (); UpdateWrapModel (); @@ -3104,7 +3252,7 @@ public class TextView : View { SetWrapModel (); _model.LoadCells (cells, ColorScheme?.Focus); - _historyText.Clear (Text); + _historyText.Clear (_model.GetAllLines ()); ResetPosition (); SetNeedsDisplay (); UpdateWrapModel (); @@ -3118,7 +3266,7 @@ public class TextView : View SetWrapModel (); InheritsPreviousAttribute = true; _model.LoadListCells (cellsList, ColorScheme?.Focus); - _historyText.Clear (Text); + _historyText.Clear (_model.GetAllLines ()); ResetPosition (); SetNeedsDisplay (); UpdateWrapModel (); @@ -4069,6 +4217,14 @@ public class TextView : View null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo) + ), + new ( + Strings.ctxColors, + "", + () => PromptForColors (), + null, + null, + (KeyCode)KeyBindings.GetKeyFromCommands (Command.Open) ) } ); @@ -4460,7 +4616,7 @@ public class TextView : View // Returns a string with the text in the selected // region. // - private string GetRegion ( + internal string GetRegion ( out List> cellsList, int? sRow = null, int? sCol = null, @@ -4536,7 +4692,7 @@ public class TextView : View OnUnwrappedCursorPosition (cRow, cCol); - return GetRegion (out _selectedCellsList, sRow: startRow, sCol: startCol, cRow: cRow, cCol: cCol, model: model); + return GetRegion (out _, sRow: startRow, sCol: startCol, cRow: cRow, cCol: cCol, model: model); } private (int Row, int Col) GetUnwrappedPosition (int line, int col) @@ -4588,12 +4744,12 @@ public class TextView : View for (var i = 0; i < obj.Lines.Count; i++) { - if (i == 0) + if (i == 0 || obj.LineStatus == HistoryText.LineStatus.Original || obj.LineStatus == HistoryText.LineStatus.Attribute) { _model.ReplaceLine (startLine, obj.Lines [i]); } - else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed) - || (!obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added)) + else if (obj is { IsUndoing: true, LineStatus: HistoryText.LineStatus.Removed } + or { IsUndoing: false, LineStatus: HistoryText.LineStatus.Added }) { _model.AddLine (startLine, obj.Lines [i]); } @@ -4671,7 +4827,7 @@ public class TextView : View List line = GetCurrentLine (); - _historyText.Add (new () { new (line) }, CursorPosition); + _historyText.Add ([new (line)], CursorPosition); // Optimize single line if (lines.Count == 1) @@ -4680,7 +4836,7 @@ public class TextView : View CurrentColumn += lines [0].Count; _historyText.Add ( - new () { new (line) }, + [new (line)], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -4709,7 +4865,7 @@ public class TextView : View } List? rest = null; - var lastp = 0; + var lastPosition = 0; if (_model.Count > 0 && line.Count > 0 && !_copyWithoutSelection) { @@ -4724,19 +4880,19 @@ public class TextView : View //model.AddLine (currentRow, lines [0]); - List> addedLines = new () { new (line) }; + List> addedLines = [new (line)]; for (var i = 1; i < lines.Count; i++) { _model.AddLine (CurrentRow + i, lines [i]); - addedLines.Add (new (lines [i])); + addedLines.Add ([..lines [i]]); } if (rest is { }) { List last = _model.GetLine (CurrentRow + lines.Count - 1); - lastp = last.Count; + lastPosition = last.Count; last.InsertRange (last.Count, rest); addedLines.Last ().InsertRange (addedLines.Last ().Count, rest); @@ -4746,11 +4902,11 @@ public class TextView : View // Now adjust column and row positions CurrentRow += lines.Count - 1; - CurrentColumn = rest is { } ? lastp : lines [lines.Count - 1].Count; + CurrentColumn = rest is { } ? lastPosition : lines [^1].Count; Adjust (); _historyText.Add ( - new () { new (line) }, + [new (line)], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -4769,7 +4925,7 @@ public class TextView : View SetWrapModel (); - _historyText.Add (new () { new (GetCurrentLine ()) }, CursorPosition); + _historyText.Add ([new (GetCurrentLine ())], CursorPosition); if (Selecting) { @@ -4807,7 +4963,7 @@ public class TextView : View } _historyText.Add ( - new () { new (GetCurrentLine ()) }, + [new (GetCurrentLine ())], CursorPosition, HistoryText.LineStatus.Replaced ); diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index ec17e6a39..97329d0fb 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -206,23 +206,11 @@ public class Editor : Scenario new MenuItem ( "Colors", "", - () => - { - if (!PromptForColor ( - "Colors", - GetSelectedCellAttribute (), - out Attribute newAttribute - )) - { - return; - } - - var attribute = new Attribute (newAttribute.Foreground, - newAttribute.Background - ); - - ApplyCellAttribute (attribute); - }) + () => _textView.PromptForColors (), + null, + null, + KeyCode.CtrlMask | KeyCode.L + ) } ), new ( @@ -337,165 +325,6 @@ public class Editor : Scenario } - private void ApplyCellAttribute (Attribute attribute) - { - if (!_textView.ReadOnly && _textView.SelectedLength > 0) - { - var startRow = Math.Min (_textView.SelectionStartRow, _textView.CurrentRow); - var endRow = Math.Max (_textView.CurrentRow, _textView.SelectionStartRow); - var startCol = _textView.SelectionStartRow <= _textView.CurrentRow ? _textView.SelectionStartColumn : _textView.CurrentColumn; - var endCol = _textView.CurrentRow >= _textView.SelectionStartRow ? _textView.CurrentColumn : _textView.SelectionStartColumn; - - for (int r = startRow; r <= endRow; r++) - { - List line = _textView.GetLine (r); - - 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 - } - } - } - } - - private Attribute? GetSelectedCellAttribute () - { - List line; - - if (_textView.SelectedLength > 0) - { - line = _textView.GetLine (_textView.SelectionStartRow); - - if (line [Math.Min (_textView.SelectionStartColumn, line.Count - 1)].Attribute is { } attributeSel) - { - return new (attributeSel); - } - - return new (_textView.ColorScheme!.Focus); - } - - line = _textView.GetCurrentLine (); - - if (line [Math.Min (_textView.CurrentColumn, line.Count - 1)].Attribute is { } attribute) - { - return new (attribute); - } - - return new (_textView.ColorScheme!.Focus); - } - - public static bool PromptForColor (string title, Attribute? current, out Attribute newAttribute) - { - var accept = false; - - var d = new Dialog - { - Title = title, - Width = Application.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), - Height = 20 - }; - - var btnOk = new Button - { - X = Pos.Center () - 5, - Y = Application.Force16Colors ? 6 : 4, - Text = "Ok", - Width = Dim.Auto (), - IsDefault = true - }; - - btnOk.Accept += (s, e) => - { - accept = true; - e.Handled = true; - Application.RequestStop (); - }; - - var btnCancel = new Button - { - X = Pos.Center () + 5, - Y = 4, - Text = "Cancel", - Width = Dim.Auto () - }; - - btnCancel.Accept += (s, e) => - { - e.Handled = true; - Application.RequestStop (); - }; - - d.Add (btnOk); - d.Add (btnCancel); - - d.AddButton (btnOk); - d.AddButton (btnCancel); - - View cpForeground; - if (Application.Force16Colors) - { - cpForeground = new ColorPicker16 - { - SelectedColor = current!.Value.Foreground.GetClosestNamedColor16 (), - Width = Dim.Fill (), - BorderStyle = LineStyle.Single, - Title = "Foreground" - }; - } - else - { - cpForeground = new ColorPicker - { - SelectedColor = current!.Value.Foreground, - Width = Dim.Fill (), - Style = new () { ShowColorName = true, ShowTextFields = true }, - BorderStyle = LineStyle.Single, - Title = "Foreground" - }; - ((ColorPicker)cpForeground).ApplyStyleChanges (); - } - - View cpBackground; - if (Application.Force16Colors) - { - cpBackground = new ColorPicker16 - { - SelectedColor = current!.Value.Background.GetClosestNamedColor16 (), - Y = Pos.Bottom (cpForeground) + 1, - Width = Dim.Fill (), - BorderStyle = LineStyle.Single, - Title = "Background" - }; - } - else - { - cpBackground = new ColorPicker - { - SelectedColor = current!.Value.Background, - Width = Dim.Fill (), - Y = Pos.Bottom (cpForeground) + 1, - Style = new () { ShowColorName = true, ShowTextFields = true }, - BorderStyle = LineStyle.Single, - Title = "Background" - }; - ((ColorPicker)cpBackground).ApplyStyleChanges (); - } - - d.Add (cpForeground, cpBackground); - - Application.Run (d); - d.Dispose (); - var newForeColor = Application.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor; - var newBackColor = Application.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor; - newAttribute = new (newForeColor, newBackColor); - - return accept; - } - private bool CanCloseFile () { if (_textView.Text == Encoding.Unicode.GetString (_originalText)) diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index fcc327eb4..b61055314 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -4713,6 +4713,86 @@ This is the second line. Assert.Equal (new Point (0, 1), tv.CursorPosition); } + [Fact] + public void HistoryText_Undo_Redo_ApplyCellsAttribute () + { + var text = "This is the first line.\nThis is the second line.\nThis is the third line."; + var tv = new TextView { Text = text }; + + tv.SelectionStartColumn = 12; + tv.CursorPosition = new Point (18, 1); + + Assert.Equal (31, tv.SelectedLength); + Assert.Equal ($"first line.{Environment.NewLine}This is the second", tv.SelectedText); + Assert.Equal ($"first line.{Environment.NewLine}This is the second", Cell.ToString (tv.SelectedCellsList)); + Assert.Equal (new Point (18, 1), tv.CursorPosition); + Assert.False (tv.IsDirty); + + AssertNullAttribute (); + + tv.ApplyCellsAttribute (new (Color.Red, Color.Green)); + + AssertRedGreenAttribute (); + + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.Equal ($"first line.{Environment.NewLine}This is the second", Cell.ToString (tv.SelectedCellsList)); + Assert.Equal (new Point (18, 1), tv.CursorPosition); + Assert.True (tv.IsDirty); + + // Undo + Assert.True (tv.NewKeyDownEvent (Key.Z.WithCtrl)); + + AssertNullAttribute (); + + Assert.Equal (12, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.Empty (tv.SelectedCellsList); + Assert.Equal (new Point (12, 0), tv.CursorPosition); + Assert.False (tv.IsDirty); + + // Redo + Assert.True (tv.NewKeyDownEvent (Key.R.WithCtrl)); + + AssertRedGreenAttribute (); + + Assert.Equal (12, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.Empty (tv.SelectedCellsList); + Assert.Equal (new Point (12, 0), tv.CursorPosition); + Assert.True (tv.IsDirty); + + void AssertNullAttribute () + { + tv.GetRegion (out List> region, 0, 12, 1, 18); + + foreach (List cells in region) + { + foreach (Cell cell in cells) + { + Assert.Null (cell.Attribute); + } + } + } + + void AssertRedGreenAttribute () + { + tv.GetRegion (out List> region, 0, 12, 1, 18); + + foreach (List cells in region) + { + foreach (Cell cell in cells) + { + Assert.Equal ("[Red,Green]", cell.Attribute.ToString ()); + } + } + } + } + [Fact] public void Internal_Tests () {