This commit is contained in:
Charlie Kindel
2020-06-30 07:22:20 -07:00
parent e183bf0f40
commit a0522be496
9 changed files with 258 additions and 79 deletions

View File

@@ -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;
}
}
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Gets the formatted lines.
/// Gets the formatted lines.
/// </summary>
/// <remarks>
/// <para>
/// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
/// <see cref="Format(ustring, int, TextAlignment, bool)"/> will be called internally.
/// </para>
/// </remarks>
public List<ustring> Lines {
get {
// With this check, we protect against subclasses with overrides of Text
if (ustring.IsNullOrEmpty (Text)) {
lines = new List<ustring> ();
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;
}
}
/// <summary>
/// Sets a flag indicating the text needs to be formatted.
/// Subsequent calls to <see cref="Draw"/>, <see cref="Lines"/>, etc... will cause the formatting to happen.>
/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute)"/> is called.
/// If it is <c>false</c> when Draw is called, the Draw call will be faster.
/// </summary>
public void SetNeedsFormat ()
{
needsFormat = true;
}
/// <remarks>
/// <para>
/// This is set to true when the properties of <see cref="TextFormatter"/> are set.
/// </para>
/// </remarks>
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 {
/// <para>
/// If <c>width</c> is 0, a single, empty line will be returned.
/// </para>
/// <para>
/// If <c>width</c> is int.MaxValue, the text will be formatted to the maximum width possible.
/// </para>
/// </remarks>
public static List<ustring> 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 {
}
/// <summary>
/// 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.
/// </summary>
/// <returns>Max width of lines.</returns>
/// <param name="text">Text, may contain newlines.</param>
@@ -528,12 +548,12 @@ namespace Terminal.Gui {
/// <param name="hotColor">The color to use to draw the hotkey</param>
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);
}
}
}

View File

@@ -122,7 +122,7 @@ namespace Terminal.Gui {
View focused = null;
Direction focusDirection;
TextFormatter viewText;
TextFormatter textFormatter;
/// <summary>
/// Event fired when the view gets focus.
@@ -152,12 +152,12 @@ namespace Terminal.Gui {
/// <summary>
/// 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.
/// </summary>
public Key HotKey { get => viewText.HotKey; set => viewText.HotKey = value; }
public Key HotKey { get => textFormatter.HotKey; set => textFormatter.HotKey = value; }
/// <summary>
/// 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'.
/// </summary>
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 {
/// </remarks>
public View (Rect frame)
{
viewText = new TextFormatter ();
textFormatter = new TextFormatter ();
this.Text = ustring.Empty;
this.Frame = frame;
@@ -444,7 +444,7 @@ namespace Terminal.Gui {
/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
public View (Rect rect, ustring text) : this (rect)
{
viewText = new TextFormatter ();
textFormatter = new TextFormatter ();
this.Text = text;
}
@@ -464,7 +464,7 @@ namespace Terminal.Gui {
/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
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;
}
/// <summary>
@@ -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 {
/// </para>
/// </remarks>
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 {
/// </summary>
/// <value>The text alignment.</value>
public virtual TextAlignment TextAlignment {
get => viewText.Alignment;
get => textFormatter.Alignment;
set {
viewText.Alignment = value;
textFormatter.Alignment = value;
SetNeedsDisplay ();
}
}

View File

@@ -55,6 +55,21 @@ namespace Terminal.Gui {
/// </remarks>
public Action Clicked;
///// <inheritdoc/>
//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 ();
// }
//}
/// <summary>
/// Method invoked when a mouse event is generated
/// </summary>

View File

@@ -33,7 +33,7 @@ namespace Terminal.Gui {
public bool ReadOnly { get; set; } = false;
/// <summary>
/// Changed event, raised when the text has clicked.
/// Changed event, raised when the text has changed.
/// </summary>
/// <remarks>
/// This event is raised when the <see cref="Text"/> changes.

View File

@@ -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))

View File

@@ -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 ();

View File

@@ -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<TimeSpan> e)
{
_labelMirroringTimeField.Text = _timeField.Text;
}
}

View File

@@ -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);

View File

@@ -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 ()
{