From d95fe1466c5affe1502877466b58cc5b0e44d300 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 14 Jan 2024 23:02:54 +0000 Subject: [PATCH 1/2] Fixes #3160. TextField doesn't update correctly the CursorPosition on Paste. (#3161) * Remove the old short two digits year and done some cleanup. * Fixes #3160. TextField doesn't update correctly the CursorPosition on Paste. * Using TextChanging instead of TextChanged event. * Removes old IsShortFormat. * Removing unnecessary private fields and done code cleanup. * Removes unnecessary GetShortDatePattern method, * Fix AdjCursorPosition method. * Create TestDateAttribute. * Reduces indentation and removes unused using. * Remove location from constructors parameters. --------- Co-authored-by: Tig From 96f75970509a37c1033dabfefcd9e3bbad663ac1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 14 Jan 2024 23:03:03 +0000 Subject: [PATCH 2/2] Fixes #3166. TimeField should use TextChanging instead of TextChanged event. (#3167) * Reduces indentation by remove the namespace braces. * Prefix private fields with underscore. * Rename to SetInitialProperties. * Reduces indentation and removes unused using. * Using TextChanging instead of TextChanged event. * Fixes #3160. TextField doesn't update correctly the CursorPosition on Paste. * Rearrange code. * Fix key bindings. * Fix Non-numerics are ignored. * Add format normalization. * Improve cursor position adjustments. * Ensures clear selection if it isn't selecting. * Provides more text validation. * Unit test with selection. --------- Co-authored-by: Tig --- Terminal.Gui/Views/TimeField.cs | 640 ++++++++++++++++-------------- UnitTests/Views/TimeFieldTests.cs | 247 ++++++++---- 2 files changed, 509 insertions(+), 378 deletions(-) diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index ee45f1ad1..bc6e7c6fe 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -9,334 +9,394 @@ using System.Globalization; using System.Linq; using System.Text; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Time editing +/// +/// +/// The provides time editing functionality with mouse support. +/// +public class TimeField : TextField { + TimeSpan _time; + bool _isShort; + + int _longFieldLen = 8; + int _shortFieldLen = 5; + string _sepChar; + string _longFormat; + string _shortFormat; + + int _fieldLen => _isShort ? _shortFieldLen : _longFieldLen; + string _format => _isShort ? _shortFormat : _longFormat; + /// - /// Time editing + /// TimeChanged event, raised when the Date has changed. /// /// - /// The provides time editing functionality with mouse support. + /// This event is raised when the changes. /// - public class TimeField : TextField { - TimeSpan time; - bool isShort; + /// + /// The passed is a containing the old value, new value, and format string. + /// + public event EventHandler> TimeChanged; - int longFieldLen = 8; - int shortFieldLen = 5; - string sepChar; - string longFormat; - string shortFormat; + /// + /// Initializes a new instance of using positioning. + /// + /// The x coordinate. + /// The y coordinate. + /// Initial time. + /// If true, the seconds are hidden. Sets the property. + public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "") + { + SetInitialProperties (time, isShort); + } - int fieldLen => isShort ? shortFieldLen : longFieldLen; - string format => isShort ? shortFormat : longFormat; + /// + /// Initializes a new instance of using positioning. + /// + /// Initial time + public TimeField (TimeSpan time) : base (string.Empty) + { + Width = _fieldLen + 2; + SetInitialProperties (time); + } - /// - /// TimeChanged event, raised when the Date has changed. - /// - /// - /// This event is raised when the changes. - /// - /// - /// The passed is a containing the old value, new value, and format string. - /// - public event EventHandler> TimeChanged; + /// + /// Initializes a new instance of using positioning. + /// + public TimeField () : this (time: TimeSpan.MinValue) { } - /// - /// Initializes a new instance of using positioning. - /// - /// The x coordinate. - /// The y coordinate. - /// Initial time. - /// If true, the seconds are hidden. Sets the property. - public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "") - { - Initialize (time, isShort); - } + void SetInitialProperties (TimeSpan time, bool isShort = false) + { + CultureInfo cultureInfo = CultureInfo.CurrentCulture; + _sepChar = cultureInfo.DateTimeFormat.TimeSeparator; + _longFormat = $" hh\\{_sepChar}mm\\{_sepChar}ss"; + _shortFormat = $" hh\\{_sepChar}mm"; + this._isShort = isShort; + Time = time; + CursorPosition = 1; + TextChanging += TextField_TextChanging; - /// - /// Initializes a new instance of using positioning. - /// - /// Initial time - public TimeField (TimeSpan time) : base (string.Empty) - { - Width = fieldLen + 2; - Initialize (time); - } + // Things this view knows how to do + AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; }); + AddCommand (Command.LeftHome, () => MoveHome ()); + AddCommand (Command.Left, () => MoveLeft ()); + AddCommand (Command.RightEnd, () => MoveEnd ()); + AddCommand (Command.Right, () => MoveRight ()); - /// - /// Initializes a new instance of using positioning. - /// - public TimeField () : this (time: TimeSpan.MinValue) { } + // Default keybindings for this view + KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight); + KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); - void Initialize (TimeSpan time, bool isShort = false) - { - CultureInfo cultureInfo = CultureInfo.CurrentCulture; - sepChar = cultureInfo.DateTimeFormat.TimeSeparator; - longFormat = $" hh\\{sepChar}mm\\{sepChar}ss"; - shortFormat = $" hh\\{sepChar}mm"; - this.isShort = isShort; - Time = time; - CursorPosition = 1; - TextChanged += TextField_TextChanged; + KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft); - // Things this view knows how to do - AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); - AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; }); - AddCommand (Command.LeftHome, () => MoveHome ()); - AddCommand (Command.Left, () => MoveLeft ()); - AddCommand (Command.RightEnd, () => MoveEnd ()); - AddCommand (Command.Right, () => MoveRight ()); + KeyBindings.Add (Key.Home, Command.LeftHome); + KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome); - // Default keybindings for this view - KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight); - KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight); + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.B.WithCtrl, Command.Left); - KeyBindings.Add (KeyCode.Delete, Command.DeleteCharLeft); - KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (Key.End, Command.RightEnd); + KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); - KeyBindings.Add (KeyCode.Home, Command.LeftHome); - KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.LeftHome); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.F.WithCtrl, Command.Right); + } - KeyBindings.Add (KeyCode.CursorLeft, Command.Left); - KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left); - - KeyBindings.Add (KeyCode.End, Command.RightEnd); - KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.RightEnd); - - KeyBindings.Add (KeyCode.CursorRight, Command.Right); - KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right); - } - - void TextField_TextChanged (object sender, TextChangedEventArgs e) - { - try { - if (!TimeSpan.TryParseExact (Text.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) - Text = e.OldValue; - } catch (Exception) { - Text = e.OldValue; - } - } - - /// - /// Gets or sets the time of the . - /// - /// - /// - public TimeSpan Time { - get { - return time; - } - set { - if (ReadOnly) - return; - - var oldTime = time; - time = value; - this.Text = " " + value.ToString (format.Trim ()); - var args = new DateTimeEventArgs (oldTime, value, format); - if (oldTime != value) { - OnTimeChanged (args); + void TextField_TextChanging (object sender, TextChangingEventArgs e) + { + try { + int spaces = 0; + for (int i = 0; i < e.NewText.Length; i++) { + if (e.NewText [i] == ' ') { + spaces++; + } else { + break; } } + spaces += _fieldLen; + string trimedText = e.NewText [..spaces]; + spaces -= _fieldLen; + trimedText = trimedText.Replace (new string (' ', spaces), " "); + if (trimedText != e.NewText) { + e.NewText = trimedText; + } + if (!TimeSpan.TryParseExact (e.NewText.Trim (), _format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) { + e.Cancel = true; + } + AdjCursorPosition (CursorPosition, true); + } catch (Exception) { + e.Cancel = true; } + } - /// - /// Get or sets whether uses the short or long time format. - /// - public bool IsShortFormat { - get => isShort; - set { - isShort = value; - if (isShort) - Width = 7; - else - Width = 10; - var ro = ReadOnly; - if (ro) - ReadOnly = false; - SetText (Text); - ReadOnly = ro; - SetNeedsDisplay (); - } + /// + /// Gets or sets the time of the . + /// + /// + /// + public TimeSpan Time { + get { + return _time; } - - /// - public override int CursorPosition { - get => base.CursorPosition; - set { - base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); - } - } - - bool SetText (Rune key) - { - var text = Text.EnumerateRunes ().ToList (); - var newText = text.GetRange (0, CursorPosition); - newText.Add (key); - if (CursorPosition < fieldLen) - newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); - return SetText (StringExtensions.ToString (newText)); - } - - bool SetText (string text) - { - if (string.IsNullOrEmpty (text)) { - return false; - } - - string [] vals = text.Split (sepChar); - bool isValidTime = true; - int hour = Int32.Parse (vals [0]); - int minute = Int32.Parse (vals [1]); - int second = isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2].ToString ()) : 0; - if (hour < 0) { - isValidTime = false; - hour = 0; - vals [0] = "0"; - } else if (hour > 23) { - isValidTime = false; - hour = 23; - vals [0] = "23"; - } - if (minute < 0) { - isValidTime = false; - minute = 0; - vals [1] = "0"; - } else if (minute > 59) { - isValidTime = false; - minute = 59; - vals [1] = "59"; - } - if (second < 0) { - isValidTime = false; - second = 0; - vals [2] = "0"; - } else if (second > 59) { - isValidTime = false; - second = 59; - vals [2] = "59"; - } - string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}"; - - if (!TimeSpan.TryParseExact (t.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) || - !isValidTime) - return false; - Time = result; - return true; - } - - void IncCursorPosition () - { - if (CursorPosition == fieldLen) + set { + if (ReadOnly) return; - if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; - } - void DecCursorPosition () - { - if (CursorPosition == 1) - return; - if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition--; - } - - void AdjCursorPosition () - { - if (Text [CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; - } - - /// - public override bool OnProcessKeyDown (Key a) - { - // Ignore non-numeric characters. - if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9) { - if (!ReadOnly) { - if (SetText ((Rune)a)) { - IncCursorPosition (); - } - } - return true; + var oldTime = _time; + _time = value; + this.Text = " " + value.ToString (_format.Trim ()); + var args = new DateTimeEventArgs (oldTime, value, _format); + if (oldTime != value) { + OnTimeChanged (args); } + } + } - if (a.IsKeyCodeAtoZ) { - return true; - } - + /// + /// Get or sets whether uses the short or long time format. + /// + public bool IsShortFormat { + get => _isShort; + set { + _isShort = value; + if (_isShort) + Width = 7; + else + Width = 10; + var ro = ReadOnly; + if (ro) + ReadOnly = false; + SetText (Text); + ReadOnly = ro; + SetNeedsDisplay (); + } + } + + /// + public override int CursorPosition { + get => base.CursorPosition; + set { + base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1); + } + } + + bool SetText (Rune key) + { + var text = Text.EnumerateRunes ().ToList (); + var newText = text.GetRange (0, CursorPosition); + newText.Add (key); + if (CursorPosition < _fieldLen) + newText = [.. newText, .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))]; + return SetText (StringExtensions.ToString (newText)); + } + + bool SetText (string text) + { + if (string.IsNullOrEmpty (text)) { return false; } - bool MoveRight () - { - IncCursorPosition (); - return true; + text = NormalizeFormat (text); + string [] vals = text.Split (_sepChar); + bool isValidTime = true; + int hour = Int32.Parse (vals [0]); + int minute = Int32.Parse (vals [1]); + int second = _isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2]) : 0; + if (hour < 0) { + isValidTime = false; + hour = 0; + vals [0] = "0"; + } else if (hour > 23) { + isValidTime = false; + hour = 23; + vals [0] = "23"; + } + if (minute < 0) { + isValidTime = false; + minute = 0; + vals [1] = "0"; + } else if (minute > 59) { + isValidTime = false; + minute = 59; + vals [1] = "59"; + } + if (second < 0) { + isValidTime = false; + second = 0; + vals [2] = "0"; + } else if (second > 59) { + isValidTime = false; + second = 59; + vals [2] = "59"; + } + string t = _isShort ? $" {hour,2:00}{_sepChar}{minute,2:00}" : $" {hour,2:00}{_sepChar}{minute,2:00}{_sepChar}{second,2:00}"; + + if (!TimeSpan.TryParseExact (t.Trim (), _format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) || + !isValidTime) { + return false; + } + Time = result; + return true; + } + + string NormalizeFormat (string text, string fmt = null, string sepChar = null) + { + if (string.IsNullOrEmpty (fmt)) { + fmt = _format; + } + fmt = fmt.Replace ("\\", ""); + if (string.IsNullOrEmpty (sepChar)) { + sepChar = _sepChar; + } + if (fmt.Length != text.Length) { + return text; } - new bool MoveEnd () - { - CursorPosition = fieldLen; - return true; + var fmtText = text.ToCharArray (); + for (int i = 0; i < text.Length; i++) { + var c = fmt [i]; + if (c.ToString () == sepChar && text [i].ToString () != sepChar) { + fmtText [i] = c; + } } - bool MoveLeft () - { - DecCursorPosition (); - return true; - } + return new string (fmtText); + } - bool MoveHome () - { - // Home, C-A + void IncCursorPosition () + { + if (CursorPosition >= _fieldLen) { + CursorPosition = _fieldLen; + return; + } + CursorPosition++; + AdjCursorPosition (CursorPosition); + } + + void DecCursorPosition () + { + if (CursorPosition <= 1) { CursorPosition = 1; - return true; - } - - /// - public override void DeleteCharLeft (bool useOldCursorPos = true) - { - if (ReadOnly) - return; - - SetText ((Rune)'0'); - DecCursorPosition (); return; } + CursorPosition--; + AdjCursorPosition (CursorPosition, false); + } - /// - public override void DeleteCharRight () - { - if (ReadOnly) - return; - - SetText ((Rune)'0'); - return; + void AdjCursorPosition (int point, bool increment = true) + { + var newPoint = point; + if (point > _fieldLen) { + newPoint = _fieldLen; + } + if (point < 1) { + newPoint = 1; + } + if (newPoint != point) { + CursorPosition = newPoint; } - /// - public override bool MouseEvent (MouseEvent ev) - { - if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) - return false; - if (!HasFocus) - SetFocus (); - - var point = ev.X; - if (point > fieldLen) - point = fieldLen; - if (point < 1) - point = 1; - CursorPosition = point; - AdjCursorPosition (); - return true; - } - - /// - /// Event firing method that invokes the event. - /// - /// The event arguments - public virtual void OnTimeChanged (DateTimeEventArgs args) - { - TimeChanged?.Invoke (this, args); + while (Text [CursorPosition] == _sepChar [0]) { + if (increment) { + CursorPosition++; + } else { + CursorPosition--; + } } } -} \ No newline at end of file + + /// + public override bool OnProcessKeyDown (Key a) + { + // Ignore non-numeric characters. + if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9) { + if (!ReadOnly) { + if (SetText ((Rune)a)) { + IncCursorPosition (); + } + } + return true; + } + + return false; + } + + bool MoveRight () + { + ClearAllSelection (); + IncCursorPosition (); + return true; + } + + new bool MoveEnd () + { + ClearAllSelection (); + CursorPosition = _fieldLen; + return true; + } + + bool MoveLeft () + { + ClearAllSelection (); + DecCursorPosition (); + return true; + } + + bool MoveHome () + { + // Home, C-A + ClearAllSelection (); + CursorPosition = 1; + return true; + } + + /// + public override void DeleteCharLeft (bool useOldCursorPos = true) + { + if (ReadOnly) { + return; + } + + ClearAllSelection (); + SetText ((Rune)'0'); + DecCursorPosition (); + return; + } + + /// + public override void DeleteCharRight () + { + if (ReadOnly) { + return; + } + + ClearAllSelection (); + SetText ((Rune)'0'); + return; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + var result = base.MouseEvent (ev); + + if (result && SelectedLength == 0 && ev.Flags.HasFlag (MouseFlags.Button1Pressed)) { + int point = ev.X; + AdjCursorPosition (point, true); + } + return result; + } + + /// + /// Event firing method that invokes the event. + /// + /// The event arguments + public virtual void OnTimeChanged (DateTimeEventArgs args) + { + TimeChanged?.Invoke (this, args); + } +} diff --git a/UnitTests/Views/TimeFieldTests.cs b/UnitTests/Views/TimeFieldTests.cs index 1b4c3afd3..e685bdd47 100644 --- a/UnitTests/Views/TimeFieldTests.cs +++ b/UnitTests/Views/TimeFieldTests.cs @@ -1,99 +1,170 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; -namespace Terminal.Gui.ViewsTests { - public class TimeFieldTests { - [Fact] - public void Constructors_Defaults () - { - var tf = new TimeField (); - Assert.False (tf.IsShortFormat); - Assert.Equal (TimeSpan.MinValue, tf.Time); - Assert.Equal (1, tf.CursorPosition); - Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); +namespace Terminal.Gui.ViewsTests; +public class TimeFieldTests { + [Fact] + public void Constructors_Defaults () + { + var tf = new TimeField (); + Assert.False (tf.IsShortFormat); + Assert.Equal (TimeSpan.MinValue, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); - var time = DateTime.Now.TimeOfDay; - tf = new TimeField (time); - Assert.False (tf.IsShortFormat); - Assert.Equal (time, tf.Time); - Assert.Equal (1, tf.CursorPosition); - Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); + var time = DateTime.Now.TimeOfDay; + tf = new TimeField (time); + Assert.False (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); - tf = new TimeField (1, 2, time); - Assert.False (tf.IsShortFormat); - Assert.Equal (time, tf.Time); - Assert.Equal (1, tf.CursorPosition); - Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame); + tf = new TimeField (1, 2, time); + Assert.False (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame); - tf = new TimeField (3, 4, time, true); - Assert.True (tf.IsShortFormat); - Assert.Equal (time, tf.Time); - Assert.Equal (1, tf.CursorPosition); - Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame); + tf = new TimeField (3, 4, time, true); + Assert.True (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame); - tf.IsShortFormat = false; - Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame); - Assert.Equal (10, tf.Width); - } + tf.IsShortFormat = false; + Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame); + Assert.Equal (10, tf.Width); + } - [Fact] - public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format () - { - var tf = new TimeField (); - Assert.Equal (1, tf.CursorPosition); - tf.CursorPosition = 0; - Assert.Equal (1, tf.CursorPosition); - tf.CursorPosition = 9; - Assert.Equal (8, tf.CursorPosition); - tf.IsShortFormat = true; - tf.CursorPosition = 0; - Assert.Equal (1, tf.CursorPosition); - tf.CursorPosition = 6; - Assert.Equal (5, tf.CursorPosition); - } + [Fact] + public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format () + { + var tf = new TimeField (); + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 0; + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 9; + Assert.Equal (8, tf.CursorPosition); + tf.IsShortFormat = true; + tf.CursorPosition = 0; + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 6; + Assert.Equal (5, tf.CursorPosition); + } - [Fact] - public void KeyBindings_Command () - { - TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19")); - tf.ReadOnly = true; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete))); - Assert.Equal (" 12:12:19", tf.Text); - tf.ReadOnly = false; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.CtrlMask))); - Assert.Equal (" 02:12:19", tf.Text); - tf.CursorPosition = 4; - tf.ReadOnly = true; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete))); - Assert.Equal (" 02:12:19", tf.Text); - tf.ReadOnly = false; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.Backspace))); - Assert.Equal (" 02:02:19", tf.Text); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.Home))); - Assert.Equal (1, tf.CursorPosition); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.End))); - Assert.Equal (8, tf.CursorPosition); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.A | KeyCode.CtrlMask))); - Assert.Equal (1, tf.CursorPosition); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.E | KeyCode.CtrlMask))); - Assert.Equal (8, tf.CursorPosition); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft))); - Assert.Equal (7, tf.CursorPosition); - Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight))); - Assert.Equal (8, tf.CursorPosition); - // Non-numerics are ignored - Assert.True (tf.NewKeyDownEvent (new (KeyCode.A))); - tf.ReadOnly = true; - tf.CursorPosition = 1; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1))); - Assert.Equal (" 02:02:19", tf.Text); - tf.ReadOnly = false; - Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1))); - Assert.Equal (" 12:02:19", tf.Text); - } + [Fact] + public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format_After_Selection () + { + var tf = new TimeField (); + // Start selection + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft | KeyCode.ShiftMask))); + Assert.Equal (1, tf.SelectedStart); + Assert.Equal (1, tf.SelectedLength); + Assert.Equal (0, tf.CursorPosition); + // Without selection + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft))); + Assert.Equal (-1, tf.SelectedStart); + Assert.Equal (0, tf.SelectedLength); + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 8; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask))); + Assert.Equal (8, tf.SelectedStart); + Assert.Equal (1, tf.SelectedLength); + Assert.Equal (9, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight))); + Assert.Equal (-1, tf.SelectedStart); + Assert.Equal (0, tf.SelectedLength); + Assert.Equal (8, tf.CursorPosition); + Assert.False (tf.IsShortFormat); + tf.IsShortFormat = true; + Assert.Equal (5, tf.CursorPosition); + // Start selection + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask))); + Assert.Equal (5, tf.SelectedStart); + Assert.Equal (1, tf.SelectedLength); + Assert.Equal (6, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight))); + Assert.Equal (-1, tf.SelectedStart); + Assert.Equal (0, tf.SelectedLength); + Assert.Equal (5, tf.CursorPosition); + } + + [Fact] + public void KeyBindings_Command () + { + TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19")); + tf.ReadOnly = true; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete))); + Assert.Equal (" 12:12:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.CtrlMask))); + Assert.Equal (" 02:12:19", tf.Text); + tf.CursorPosition = 4; + tf.ReadOnly = true; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete))); + Assert.Equal (" 02:12:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.Backspace))); + Assert.Equal (" 02:02:19", tf.Text); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.Home))); + Assert.Equal (1, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.End))); + Assert.Equal (8, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.A | KeyCode.CtrlMask))); + Assert.Equal (1, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.E | KeyCode.CtrlMask))); + Assert.Equal (8, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft))); + Assert.Equal (7, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight))); + Assert.Equal (8, tf.CursorPosition); + // Non-numerics are ignored + Assert.False (tf.NewKeyDownEvent (new (KeyCode.A))); + tf.ReadOnly = true; + tf.CursorPosition = 1; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1))); + Assert.Equal (" 02:02:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1))); + Assert.Equal (" 12:02:19", tf.Text); + Assert.Equal (2, tf.CursorPosition); + Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.AltMask))); + Assert.Equal (" 10:02:19", tf.Text); + } + + [Fact] + public void Typing_With_Selection_Normalize_Format () + { + TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19")); + // Start selection at before the first separator : + tf.CursorPosition = 2; + // Now select the separator : + Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask))); + Assert.Equal (2, tf.SelectedStart); + Assert.Equal (1, tf.SelectedLength); + Assert.Equal (3, tf.CursorPosition); + // Type 3 over the separator + Assert.True (tf.NewKeyDownEvent (new (KeyCode.D3))); + // The format was normalized and replaced again with : + Assert.Equal (" 12:12:19", tf.Text); + Assert.Equal (4, tf.CursorPosition); + } + + [Fact, AutoInitShutdown] + public void Copy_Paste () + { + TimeField tf1 = new TimeField (TimeSpan.Parse ("12:12:19")); + TimeField tf2 = new TimeField (TimeSpan.Parse ("12:59:01")); + // Select all text + Assert.True (tf2.NewKeyDownEvent (new (KeyCode.End | KeyCode.ShiftMask))); + Assert.Equal (1, tf2.SelectedStart); + Assert.Equal (8, tf2.SelectedLength); + Assert.Equal (9, tf2.CursorPosition); + // Copy from tf2 + Assert.True (tf2.NewKeyDownEvent (new (KeyCode.C | KeyCode.CtrlMask))); + // Paste into tf1 + Assert.True (tf1.NewKeyDownEvent (new (KeyCode.V | KeyCode.CtrlMask))); + Assert.Equal (" 12:59:01", tf1.Text); + Assert.Equal (9, tf1.CursorPosition); } }