diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs index e2a4b7221..53db10452 100644 --- a/Terminal.Gui/Core/TextFormatter.cs +++ b/Terminal.Gui/Core/TextFormatter.cs @@ -35,7 +35,7 @@ namespace Terminal.Gui { ustring text; TextAlignment textAlignment; Attribute textColor = -1; - bool needsFormat = true; + bool needsFormat; Key hotKey; Size size; @@ -46,7 +46,14 @@ namespace Terminal.Gui { get => text; set { text = value; - needsFormat = true; + + if (Size.IsEmpty) { + // Proivde a default size (width = length of longest line, height = 1) + // TODO: It might makem more sense for the default to be width = length of first line? + Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1); + } + + NeedsFormat = true; } } @@ -59,18 +66,18 @@ namespace Terminal.Gui { get => textAlignment; set { textAlignment = value; - needsFormat = true; + NeedsFormat = true; } } /// - /// Gets the size of the area the text will be drawn in. + /// Gets or sets the size of the area the text will be constrainted to when formatted. /// public Size Size { get => size; - internal set { + set { size = value; - needsFormat = true; + NeedsFormat = true; } } @@ -96,40 +103,50 @@ namespace Terminal.Gui { public uint HotKeyTagMask { get; set; } = 0x100000; /// - /// Gets the formatted lines. + /// Gets the formatted lines. /// + /// + /// + /// Upon a 'get' of this property, if the text needs to be formatted (if is true) + /// will be called internally. + /// + /// public List Lines { get { // With this check, we protect against subclasses with overrides of Text if (ustring.IsNullOrEmpty (Text)) { lines = new List (); lines.Add (ustring.Empty); - needsFormat = false; + NeedsFormat = false; return lines; } - if (needsFormat) { + if (NeedsFormat) { var shown_text = text; if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) { shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier); shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos); } + if (Size.IsEmpty) { + throw new InvalidOperationException ("Size must be set before accessing Lines"); + } lines = Format (shown_text, Size.Width, textAlignment, Size.Height > 1); + NeedsFormat = false; } - needsFormat = false; return lines; } } /// - /// Sets a flag indicating the text needs to be formatted. - /// Subsequent calls to , , etc... will cause the formatting to happen.> + /// Gets or sets whether the needs to format the text when is called. + /// If it is false when Draw is called, the Draw call will be faster. /// - public void SetNeedsFormat () - { - needsFormat = true; - } - + /// + /// + /// This is set to true when the properties of are set. + /// + /// + public bool NeedsFormat { get => needsFormat; set => needsFormat = value; } static ustring StripCRLF (ustring str) { @@ -199,14 +216,14 @@ namespace Terminal.Gui { return lines; } - var runes = StripCRLF (text).ToRuneList(); + var runes = StripCRLF (text).ToRuneList (); while ((end = start + width) < runes.Count) { while (runes [end] != ' ' && end > start) end -= 1; if (end == start) end = start + width; - lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace()); + lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace ()); start = end; } @@ -236,7 +253,7 @@ namespace Terminal.Gui { var runes = text.ToRuneList (); int slen = runes.Count; if (slen > width) { - return ustring.Make (runes.GetRange(0, width)); + return ustring.Make (runes.GetRange (0, width)); } else { if (talign == TextAlignment.Justified) { return Justify (text, width); @@ -303,6 +320,9 @@ namespace Terminal.Gui { /// /// If width is 0, a single, empty line will be returned. /// + /// + /// If width is int.MaxValue, the text will be formatted to the maximum width possible. + /// /// public static List Format (ustring text, int width, TextAlignment talign, bool wordWrap) { @@ -329,7 +349,7 @@ namespace Terminal.Gui { for (int i = 0; i < runeCount; i++) { Rune c = text [i]; if (c == '\n') { - var wrappedLines = WordWrap (ustring.Make (runes.GetRange(lp, i - lp)), width); + var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width); foreach (var line in wrappedLines) { lineResult.Add (ClipAndJustify (line, width, talign)); } @@ -339,7 +359,7 @@ namespace Terminal.Gui { lp = i + 1; } } - foreach (var line in WordWrap (ustring.Make (runes.GetRange(lp, runeCount - lp)), width)) { + foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width)) { lineResult.Add (ClipAndJustify (line, width, talign)); } @@ -359,7 +379,7 @@ namespace Terminal.Gui { } /// - /// Computes the maximum width needed to render the text (single line or multple lines). + /// Computes the maximum width needed to render the text (single line or multple lines) given a minimium width. /// /// Max width of lines. /// Text, may contain newlines. @@ -528,12 +548,12 @@ namespace Terminal.Gui { /// The color to use to draw the hotkey public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor) { - // With this check, we protect against subclasses with overrides of Text + // With this check, we protect against subclasses with overrides of Text (like Button) if (ustring.IsNullOrEmpty (text)) { return; } - Application.Driver.SetAttribute (normalColor); + Application.Driver?.SetAttribute (normalColor); // Use "Lines" to ensure a Format (don't use "lines")) for (int line = 0; line < Lines.Count; line++) { @@ -558,17 +578,17 @@ namespace Terminal.Gui { throw new ArgumentOutOfRangeException (); } for (var col = bounds.Left; col < bounds.Left + bounds.Width; col++) { - Application.Driver.Move (col, bounds.Top + line); + Application.Driver?.Move (col, bounds.Top + line); var rune = (Rune)' '; if (col >= x && col < (x + runes.Length)) { rune = runes [col - x]; } if ((rune & HotKeyTagMask) == HotKeyTagMask) { - Application.Driver.SetAttribute (hotColor); - Application.Driver.AddRune ((Rune)((uint)rune & ~HotKeyTagMask)); - Application.Driver.SetAttribute (normalColor); + Application.Driver?.SetAttribute (hotColor); + Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask)); + Application.Driver?.SetAttribute (normalColor); } else { - Application.Driver.AddRune (rune); + Application.Driver?.AddRune (rune); } } } diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 973a4f820..d2097964c 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -122,7 +122,7 @@ namespace Terminal.Gui { View focused = null; Direction focusDirection; - TextFormatter viewText; + TextFormatter textFormatter; /// /// Event fired when the view gets focus. @@ -152,12 +152,12 @@ namespace Terminal.Gui { /// /// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire. /// - public Key HotKey { get => viewText.HotKey; set => viewText.HotKey = value; } + public Key HotKey { get => textFormatter.HotKey; set => textFormatter.HotKey = value; } /// /// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. /// - public Rune HotKeySpecifier { get => viewText.HotKeySpecifier; set => viewText.HotKeySpecifier = value; } + public Rune HotKeySpecifier { get => textFormatter.HotKeySpecifier; set => textFormatter.HotKeySpecifier = value; } internal Direction FocusDirection { get => SuperView?.FocusDirection ?? focusDirection; @@ -381,7 +381,7 @@ namespace Terminal.Gui { /// public View (Rect frame) { - viewText = new TextFormatter (); + textFormatter = new TextFormatter (); this.Text = ustring.Empty; this.Frame = frame; @@ -444,7 +444,7 @@ namespace Terminal.Gui { /// text to initialize the property with. public View (Rect rect, ustring text) : this (rect) { - viewText = new TextFormatter (); + textFormatter = new TextFormatter (); this.Text = text; } @@ -464,7 +464,7 @@ namespace Terminal.Gui { /// text to initialize the property with. public View (ustring text) : base () { - viewText = new TextFormatter (); + textFormatter = new TextFormatter (); this.Text = text; CanFocus = false; @@ -495,7 +495,7 @@ namespace Terminal.Gui { if (SuperView == null) return; SuperView.SetNeedsLayout (); - viewText.SetNeedsFormat (); + textFormatter.NeedsFormat = true; } /// @@ -888,7 +888,7 @@ namespace Terminal.Gui { focused.PositionCursor (); else { if (CanFocus && HasFocus) { - Move (viewText.HotKeyPos == -1 ? 1 : viewText.HotKeyPos, 0); + Move (textFormatter.HotKeyPos == -1 ? 1 : textFormatter.HotKeyPos, 0); } else { Move (frame.X, frame.Y); } @@ -1048,8 +1048,10 @@ namespace Terminal.Gui { if (!ustring.IsNullOrEmpty (Text)) { Clear (); // Draw any Text - viewText?.SetNeedsFormat (); - viewText?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); + if (textFormatter != null) { + textFormatter.NeedsFormat = true; + } + textFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); } // Invoke DrawContentEvent @@ -1531,7 +1533,7 @@ namespace Terminal.Gui { Rect oldBounds = Bounds; OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds }); - viewText.Size = Bounds.Size; + textFormatter.Size = Bounds.Size; // Sort out the dependencies of the X, Y, Width, Height properties @@ -1592,9 +1594,9 @@ namespace Terminal.Gui { /// /// public virtual ustring Text { - get => viewText.Text; + get => textFormatter.Text; set { - viewText.Text = value; + textFormatter.Text = value; SetNeedsDisplay (); } } @@ -1604,9 +1606,9 @@ namespace Terminal.Gui { /// /// The text alignment. public virtual TextAlignment TextAlignment { - get => viewText.Alignment; + get => textFormatter.Alignment; set { - viewText.Alignment = value; + textFormatter.Alignment = value; SetNeedsDisplay (); } } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index cd6c9d1ec..18778a86f 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -55,6 +55,21 @@ namespace Terminal.Gui { /// public Action Clicked; + ///// + //public new ustring Text { + // get => base.Text; + // set { + // base.Text = value; + // // This supports Label auto-sizing when Text changes (preserving backwards compat behavior) + // if (Frame.Height == 1 && !ustring.IsNullOrEmpty (value)) { + // int w = Text.RuneCount; + // Width = w; + // Frame = new Rect (Frame.Location, new Size (w, Frame.Height)); + // } + // SetNeedsDisplay (); + // } + //} + /// /// Method invoked when a mouse event is generated /// diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 2fa43f832..d0c7c6d60 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -33,7 +33,7 @@ namespace Terminal.Gui { public bool ReadOnly { get; set; } = false; /// - /// Changed event, raised when the text has clicked. + /// Changed event, raised when the text has changed. /// /// /// This event is raised when the changes. diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index efe8e6ee2..8e9605f2d 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -77,10 +77,10 @@ namespace Terminal.Gui { shortFormat = $" hh\\{sepChar}mm"; CursorPosition = 1; Time = time; - TextChanged += TimeField_Changed; + TextChanged += TextField_TextChanged; } - void TimeField_Changed (ustring e) + void TextField_TextChanged (ustring e) { try { if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 5b55065ef..e86372073 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -335,6 +335,7 @@ namespace UICatalog { { // Remove existing class, if any if (view != null) { + view.LayoutComplete -= LayoutCompleteHandler; _hostPane.Remove (view); view.Dispose (); _hostPane.Clear (); @@ -348,8 +349,8 @@ namespace UICatalog { //_curView.X = Pos.Center (); //_curView.Y = Pos.Center (); - //_curView.Width = Dim.Fill (5); - //_curView.Height = Dim.Fill (5); + view.Width = Dim.Percent(75); + view.Height = Dim.Percent (75); // Set the colorscheme to make it stand out view.ColorScheme = Colors.Base; @@ -386,9 +387,17 @@ namespace UICatalog { _hostPane.SetNeedsDisplay (); UpdateSettings (view); UpdateTitle (view); + + view.LayoutComplete += LayoutCompleteHandler; + return view; } + void LayoutCompleteHandler(View.LayoutEventArgs args) + { + UpdateTitle (_curView); + } + public override void Run () { base.Run (); diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs index 902106c76..86f505a14 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -9,51 +9,106 @@ namespace UICatalog { class Text : Scenario { public override void Setup () { - var s = "This is a test intended to show how TAB key works (or doesn't) across text fields."; - Win.Add (new TextField (s) { - X = 5, + var s = "TAB to jump between text fields."; + var textField = new TextField (s) { + X = 1, Y = 1, - Width = Dim.Percent (80), - ColorScheme = Colors.Dialog - }); + Width = Dim.Percent (50), + //ColorScheme = Colors.Dialog + }; + Win.Add (textField); + + var labelMirroringTextField = new Label (textField.Text) { + X = Pos.Right (textField) + 1, + Y = Pos.Top (textField), + Width = Dim.Fill (1) + }; + Win.Add (labelMirroringTextField); + + textField.TextChanged += (prev) => { + labelMirroringTextField.Text = textField.Text; + }; var textView = new TextView () { - X = 5, + X = 1, Y = 3, - Width = Dim.Percent (80), - Height = Dim.Percent (40), + Width = Dim.Percent (50), + Height = Dim.Percent (30), ColorScheme = Colors.Dialog }; textView.Text = s; Win.Add (textView); + var labelMirroringTextView = new Label (textView.Text) { + X = Pos.Right (textView) + 1, + Y = Pos.Top (textView), + Width = Dim.Fill (1), + Height = Dim.Height (textView), + }; + Win.Add (labelMirroringTextView); + + textView.TextChanged += () => { + labelMirroringTextView.Text = textView.Text; + }; + // BUGBUG: 531 - TAB doesn't go to next control from HexView - var hexView = new HexView (new System.IO.MemoryStream(Encoding.ASCII.GetBytes (s))) { - X = 5, - Y = Pos.Bottom(textView) + 1, - Width = Dim.Percent(80), - Height = Dim.Percent(40), - ColorScheme = Colors.Dialog + var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (s))) { + X = 1, + Y = Pos.Bottom (textView) + 1, + Width = Dim.Fill (1), + Height = Dim.Percent (30), + //ColorScheme = Colors.Dialog }; Win.Add (hexView); var dateField = new DateField (System.DateTime.Now) { - X = 5, + X = 1, Y = Pos.Bottom (hexView) + 1, - Width = Dim.Percent (40), - ColorScheme = Colors.Dialog, + Width = 20, + //ColorScheme = Colors.Dialog, IsShortFormat = false }; Win.Add (dateField); - var timeField = new TimeField (DateTime.Now.TimeOfDay) { - X = Pos.Right (dateField) + 5, + var labelMirroringDateField = new Label (dateField.Text) { + X = Pos.Right (dateField) + 1, + Y = Pos.Top (dateField), + Width = Dim.Width (dateField), + Height = Dim.Height (dateField), + }; + Win.Add (labelMirroringDateField); + + dateField.TextChanged += (prev) => { + labelMirroringDateField.Text = dateField.Text; + }; + + _timeField = new TimeField (DateTime.Now.TimeOfDay) { + X = Pos.Right (labelMirroringDateField) + 5, Y = Pos.Bottom (hexView) + 1, - Width = Dim.Percent (40), - ColorScheme = Colors.Dialog, + Width = 20, + //ColorScheme = Colors.Dialog, IsShortFormat = false }; - Win.Add (timeField); + Win.Add (_timeField); + + _labelMirroringTimeField = new Label (_timeField.Text) { + X = Pos.Right (_timeField) + 1, + Y = Pos.Top (_timeField), + Width = Dim.Width (_timeField), + Height = Dim.Height (_timeField), + }; + Win.Add (_labelMirroringTimeField); + + _timeField.TimeChanged += TimeChanged; + + } + + TimeField _timeField; + Label _labelMirroringTimeField; + + private void TimeChanged (DateTimeEventArgs e) + { + _labelMirroringTimeField.Text = _timeField.Text; } } diff --git a/UICatalog/Scenarios/TimeAndDate.cs b/UICatalog/Scenarios/TimeAndDate.cs index 26cd87edd..af4282aa4 100644 --- a/UICatalog/Scenarios/TimeAndDate.cs +++ b/UICatalog/Scenarios/TimeAndDate.cs @@ -53,37 +53,49 @@ namespace UICatalog { lblOldTime = new Label ("Old Time: ") { X = Pos.Center (), - Y = Pos.Bottom (longDate) + 1 + Y = Pos.Bottom (longDate) + 1, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill(), }; Win.Add (lblOldTime); lblNewTime = new Label ("New Time: ") { X = Pos.Center (), - Y = Pos.Bottom (lblOldTime) + 1 + Y = Pos.Bottom (lblOldTime) + 1, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill (), }; Win.Add (lblNewTime); lblTimeFmt = new Label ("Time Format: ") { X = Pos.Center (), - Y = Pos.Bottom (lblNewTime) + 1 + Y = Pos.Bottom (lblNewTime) + 1, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill (), }; Win.Add (lblTimeFmt); lblOldDate = new Label ("Old Date: ") { X = Pos.Center (), - Y = Pos.Bottom (lblTimeFmt) + 2 + Y = Pos.Bottom (lblTimeFmt) + 2, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill (), }; Win.Add (lblOldDate); lblNewDate = new Label ("New Date: ") { X = Pos.Center (), - Y = Pos.Bottom (lblOldDate) + 1 + Y = Pos.Bottom (lblOldDate) + 1, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill (), }; Win.Add (lblNewDate); lblDateFmt = new Label ("Date Format: ") { X = Pos.Center (), - Y = Pos.Bottom (lblNewDate) + 1 + Y = Pos.Bottom (lblNewDate) + 1, + TextAlignment = TextAlignment.Centered, + Width = Dim.Fill (), }; Win.Add (lblDateFmt); diff --git a/UnitTests/TextFormatterTests.cs b/UnitTests/TextFormatterTests.cs index 9bb8b0c57..ed795c5b0 100644 --- a/UnitTests/TextFormatterTests.cs +++ b/UnitTests/TextFormatterTests.cs @@ -16,10 +16,76 @@ namespace Terminal.Gui { [Fact] public void Basic_Usage () { + var testText = ustring.Make("test"); + var expectedSize = new Size (); + var testBounds = new Rect (0, 0, 100, 1); var tf = new TextFormatter (); + tf.Text = testText; + expectedSize = new Size (testText.Length, 1); + Assert.Equal (testText, tf.Text); + Assert.Equal (TextAlignment.Left, tf.Alignment); + Assert.Equal (expectedSize, tf.Size); + tf.Draw (testBounds, new Attribute(), new Attribute()); + Assert.Equal (expectedSize, tf.Size); + Assert.NotEmpty (tf.Lines); + tf.Alignment = TextAlignment.Right; + expectedSize = new Size (testText.Length, 1); + Assert.Equal (testText, tf.Text); + Assert.Equal (TextAlignment.Right, tf.Alignment); + Assert.Equal (expectedSize, tf.Size); + tf.Draw (testBounds, new Attribute (), new Attribute ()); + Assert.Equal (expectedSize, tf.Size); + Assert.NotEmpty (tf.Lines); + + tf.Alignment = TextAlignment.Right; + expectedSize = new Size (testText.Length * 2, 1); + tf.Size = expectedSize; + Assert.Equal (testText, tf.Text); + Assert.Equal (TextAlignment.Right, tf.Alignment); + Assert.Equal (expectedSize, tf.Size); + tf.Draw (testBounds, new Attribute (), new Attribute ()); + Assert.Equal (expectedSize, tf.Size); + Assert.NotEmpty (tf.Lines); + + tf.Alignment = TextAlignment.Centered; + expectedSize = new Size (testText.Length * 2, 1); + tf.Size = expectedSize; + Assert.Equal (testText, tf.Text); + Assert.Equal (TextAlignment.Centered, tf.Alignment); + Assert.Equal (expectedSize, tf.Size); + tf.Draw (testBounds, new Attribute (), new Attribute ()); + Assert.Equal (expectedSize, tf.Size); + Assert.NotEmpty (tf.Lines); } + + [Fact] + public void NeedsFormat_Sets () + { + var testText = ustring.Make ("test"); + var testBounds = new Rect (0, 0, 100, 1); + var tf = new TextFormatter (); + + tf.Text = "test"; + Assert.True (tf.NeedsFormat); // get_Lines causes a Format + Assert.NotEmpty (tf.Lines); + Assert.False (tf.NeedsFormat); // get_Lines causes a Format + Assert.Equal (testText, tf.Text); + tf.Draw (testBounds, new Attribute (), new Attribute ()); + Assert.False (tf.NeedsFormat); + + tf.Size = new Size (1, 1); + Assert.True (tf.NeedsFormat); + Assert.NotEmpty (tf.Lines); + Assert.False (tf.NeedsFormat); // get_Lines causes a Format + + tf.Alignment = TextAlignment.Centered; + Assert.True (tf.NeedsFormat); + Assert.NotEmpty (tf.Lines); + Assert.False (tf.NeedsFormat); // get_Lines causes a Format + } + [Fact] public void FindHotKey_Invalid_ReturnsFalse () {