diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index f6f96c495..18b7abd8f 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1,4 +1,4 @@ -// +// // Authors: // Miguel de Icaza (miguel@gnome.org) // @@ -13,6 +13,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using NStack; @@ -132,6 +133,482 @@ namespace Terminal.Gui { /// /// public class View : Responder, IEnumerable { + /// + /// Suppports text formatting, including horizontal alignment and word wrap for . + /// + public class ViewText { + List lines = new List (); + ustring text; + TextAlignment textAlignment; + Attribute textColor = -1; + View view; + + /// + /// Inititalizes a new object. + /// + /// + public ViewText (View view) + { + this.view = view; + recalcPending = true; + } + + /// + /// The text to be displayed. + /// + public virtual ustring Text { + get => text; + set { + text = value; + recalcPending = true; + view.SetNeedsDisplay (); + } + } + + // TODO: Add Vertical Text Alignment + /// + /// Controls the horizontal text-alignment property. + /// + /// The text alignment. + public TextAlignment TextAlignment { + get => textAlignment; + set { + textAlignment = value; + recalcPending = true; + view.SetNeedsDisplay (); + } + } + + /// + /// The color used for the drawing of the . + /// + public Attribute TextColor { + get => textColor; + set { + textColor = value; + recalcPending = true; + view.SetNeedsDisplay (); + } + } + + /// + /// Gets the size of the area the text will be drawn in. + /// + public Size TextSize { get; internal set; } + + bool recalcPending = false; + + public int HotKeyPos { get => hotKeyPos; set => hotKeyPos = value; } + public Rune HotKey { get => hotKey; set => hotKey = value; } + Rune hotKey; + + /// + /// 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; set; } = (Rune)0xFFFF; + + /// + /// Causes the Text to be formatted, based on and . + /// + public void ReFormat () + { + // With this check, we protect against subclasses with overrides of Text + if (ustring.IsNullOrEmpty (Text)) { + return; + } + recalcPending = false; + var shown_text = ProcessHotKeyText (text, HotKeySpecifier, false, out hotKeyPos, out hotKey); + Reformat (shown_text, lines, TextSize.Width, textAlignment, TextSize.Height > 1); + } + + static ustring StripCRLF (ustring str) + { + var runes = new List (); + foreach (var r in str.ToRunes ()) { + if (r != '\r' && r != '\n') { + runes.Add (r); + } + } + return ustring.Make (runes); ; + } + static ustring ReplaceCRLFWithSpace (ustring str) + { + var runes = new List (); + foreach (var r in str.ToRunes ()) { + if (r == '\r' || r == '\n') { + runes.Add (new Rune (' ')); // r + 0x2400)); // U+25A1 □ WHITE SQUARE + } else { + runes.Add (r); + } + } + return ustring.Make (runes); ; + } + + static List WordWrap (ustring text, int margin) + { + int start = 0, end; + var lines = new List (); + + text = StripCRLF (text); + + while ((end = start + margin) < text.Length) { + while (text [end] != ' ' && end > start) + end -= 1; + if (end == start) + end = start + margin; + + lines.Add (text [start, end]); + start = end + 1; + } + + if (start < text.Length) + lines.Add (text.Substring (start)); + + return lines; + } + + static ustring ClipAndJustify (ustring str, int width, TextAlignment talign) + { + int slen = str.RuneCount; + if (slen > width) { + var uints = str.ToRunes (width); + var runes = new Rune [uints.Length]; + for (int i = 0; i < uints.Length; i++) + runes [i] = uints [i]; + return ustring.Make (runes); + } else { + if (talign == TextAlignment.Justified) { + // TODO: ustring needs this + var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries); + int textCount = words.Sum (arg => arg.Length); + + var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0; + var extras = words.Length > 1 ? (width - textCount) % words.Length : 0; + + var s = new System.Text.StringBuilder (); + //s.Append ($"tc={textCount} sp={spaces},x={extras} - "); + for (int w = 0; w < words.Length; w++) { + var x = words [w]; + s.Append (x); + if (w + 1 < words.Length) + for (int i = 0; i < spaces; i++) + s.Append (' '); + if (extras > 0) { + //s.Append ('_'); + extras--; + } + } + return ustring.Make (s.ToString ()); + } + return str; + } + } + + static char [] whitespace = new char [] { ' ', '\t' }; + private int hotKeyPos; + + /// + /// Reformats text into lines, applying text alignment and word wraping. + /// + /// + /// + /// + /// + /// if false, forces text to fit a single line. Line breaks are converted to spaces. + static void Reformat (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap) + { + lineResult.Clear (); + + if (wordWrap == false) { + textStr = ReplaceCRLFWithSpace (textStr); + lineResult.Add (ClipAndJustify (textStr, width, talign)); + return; + } + + int textLen = textStr.Length; + int lp = 0; + for (int i = 0; i < textLen; i++) { + Rune c = textStr [i]; + if (c == '\n') { + var wrappedLines = WordWrap (textStr [lp, i], width); + foreach (var line in wrappedLines) { + lineResult.Add (ClipAndJustify (line, width, talign)); + } + if (wrappedLines.Count == 0) { + lineResult.Add (ustring.Empty); + } + lp = i + 1; + } + } + foreach (var line in WordWrap (textStr [lp, textLen], width)) { + lineResult.Add (ClipAndJustify (line, width, talign)); + } + } + + /// + /// Computes the number of lines needed to render the specified text given the width. + /// + /// Number of lines. + /// Text, may contain newlines. + /// The minimum width for the text. + public static int MaxLines (ustring text, int width) + { + var result = new List (); + ViewText.Reformat (text, result, width, TextAlignment.Left, true); + return result.Count; + } + + /// + /// Computes the maximum width needed to render the text (single line or multple lines). + /// + /// Max width of lines. + /// Text, may contain newlines. + /// The minimum width for the text. + public static int MaxWidth (ustring text, int width) + { + var result = new List (); + ViewText.Reformat (text, result, width, TextAlignment.Left, true); + return result.Max (s => s.RuneCount); + } + + internal void Draw (Rect bounds) + { + // With this check, we protect against subclasses with overrides of Text + if (ustring.IsNullOrEmpty (text)) { + return; + } + + if (recalcPending) { + ReFormat (); + } + + if (TextColor != -1) + Driver.SetAttribute (TextColor); + else + Driver.SetAttribute (view.ColorScheme.Normal); + + view.Clear (); + for (int line = 0; line < lines.Count; line++) { + if (line < bounds.Top || line >= bounds.Bottom) + continue; + var str = lines [line]; + int x; + switch (textAlignment) { + case TextAlignment.Left: + x = 0; + break; + case TextAlignment.Justified: + x = bounds.Left; + break; + case TextAlignment.Right: + x = bounds.Right - str.Length; + break; + case TextAlignment.Centered: + x = bounds.Left + (bounds.Width - str.Length) / 2; + break; + default: + throw new ArgumentOutOfRangeException (); + } + view.Move (x, line); + Driver.AddStr (str); + } + + if (HotKeyPos != -1) { + _ = GetAlignedText (lines [0], TextSize.Width, hotKeyPos, out hotKeyPos, textAlignment); + + view.Move (HotKeyPos, 0); + Driver.SetAttribute (view.HasFocus ? view.ColorScheme.HotFocus : view.ColorScheme.HotNormal); + Driver.AddRune (hotKey); + } + } + + /// + /// Calculates the rectangle requried to hold text, assuming no word wrapping. + /// + /// The x location of the rectangle + /// The y location of the rectangle + /// The text to measure + /// + public static Rect CalcRect (int x, int y, ustring text) + { + if (ustring.IsNullOrEmpty (text)) + return Rect.Empty; + + int mw = 0; + int ml = 1; + + int cols = 0; + foreach (var rune in text) { + if (rune == '\n') { + ml++; + if (cols > mw) + mw = cols; + cols = 0; + } else + cols++; + } + if (cols > mw) + mw = cols; + + return new Rect (x, y, mw, ml); + } + + + /// + /// Gets the position and Rune for the hotkey in text and removes the hotkey specifier. + /// + /// The text to manipulate. + /// The hot-key specifier (e.g. '_') to look for. + /// If true and no hotkey is found via the hotkey specifier, the first upper case char found will be the hotkey. + /// Returns the postion of the hot-key in the text. -1 if not found. + /// Returns the Rune immediately to the right of the hot-key position + /// The input text with the hotkey specifier ('_') removed. + public static ustring ProcessHotKeyText (ustring text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Rune showHotKey) + { + if (hotKeySpecifier == (Rune)0xFFFF) { + hotPos = -1; + showHotKey = (Rune)0xFFFF; + return text; + } + Rune hot_key = (Rune)0; + int hot_pos = -1; + ustring shown_text = text; + + // Use first hot_key char passed into 'hotKey'. + // TODO: Ignore hot_key of two are provided + int i = 0; + foreach (Rune c in shown_text) { + if ((char)c != 0xFFFD) { + if (c == hotKeySpecifier) { + hot_pos = i; + } else if (hot_pos > -1) { + hot_key = c; + break; + } + } + i++; + } + + // Legacy support - use first upper case char if the specifier was not found + if (hot_pos == -1 && firstUpperCase) { + i = 0; + foreach (Rune c in shown_text) { + if ((char)c != 0xFFFD) { + if (Rune.IsUpper (c)) { + hot_key = c; + hot_pos = i; + break; + } + } + i++; + } + } + else { + if (hot_pos != -1) { + // Use char after 'hotKey' + ustring start = ""; + i = 0; + foreach (Rune c in shown_text) { + start += ustring.Make (c); + i++; + if (i == hot_pos) + break; + } + var st = shown_text; + shown_text = start; + i = 0; + foreach (Rune c in st) { + i++; + if (i > hot_pos + 1) { + shown_text += ustring.Make (c); + } + } + } + } + hotPos = hot_pos; + showHotKey = hot_key; + return shown_text; + } + + /// + /// Formats a single line of text with a hot-key and . + /// + /// The text to align. + /// The maximum width for the text. + /// The hot-key position before reformatting. + /// The hot-key position after reformatting. + /// The to align to. + /// The aligned text. + public static ustring GetAlignedText (ustring shown_text, int width, int hot_pos, out int c_hot_pos, TextAlignment textAlignment) + { + int start; + var caption = shown_text; + c_hot_pos = hot_pos; + + if (width > shown_text.Length + 1) { + switch (textAlignment) { + case TextAlignment.Left: + caption += new string (' ', width - caption.RuneCount); + break; + case TextAlignment.Right: + start = width - caption.RuneCount; + caption = $"{new string (' ', width - caption.RuneCount)}{caption}"; + if (c_hot_pos > -1) { + c_hot_pos += start; + } + break; + case TextAlignment.Centered: + start = width / 2 - caption.RuneCount / 2; + caption = $"{new string (' ', start)}{caption}{new string (' ', width - caption.RuneCount - start)}"; + if (c_hot_pos > -1) { + c_hot_pos += start; + } + break; + case TextAlignment.Justified: + var words = caption.Split (" "); + var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos); + var space = (width - runeCount) / (caption.Length - wLen); + caption = ""; + for (int i = 0; i < words.Length; i++) { + if (i == words.Length - 1) { + caption += new string (' ', width - caption.RuneCount - 1); + caption += words [i]; + } else { + caption += words [i]; + } + if (i < words.Length - 1) { + caption += new string (' ', space); + } + } + if (c_hot_pos > -1) { + c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1; + } + break; + } + } + + return caption; + } + + static int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos) + { + int length = 0; + int rCount = 0; + int wHotPos = -1; + for (int i = 0; i < words.Length; i++) { + if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos) + wHotPos = i; + length += words [i].Length; + rCount += words [i].RuneCount; + } + if (wHotPos == -1 && hotPos > -1) + wHotPos = words.Length; + runeCount = rCount; + wordHotPos = wHotPos; + return length; + } + } + internal enum Direction { Forward, Backward @@ -142,6 +619,8 @@ namespace Terminal.Gui { View focused = null; Direction focusDirection; + ViewText viewText; + /// /// Event fired when the view gets focus. /// @@ -167,6 +646,27 @@ namespace Terminal.Gui { /// public Action MouseClick; + /// + /// 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 Rune HotKey { get => viewText.HotKey; set => viewText.HotKey = value; } + + /// + /// + /// + public Rune HotKeySpecifier { get => viewText.HotKeySpecifier; set => viewText.HotKeySpecifier = value; } + + /// + /// Clicked , raised when the user clicks the primary mouse button within the Bounds of this + /// or if the user presses the action key while this view is focused. (TODO: IsDefault) + /// + /// + /// Client code can hook up to this event, it is + /// raised when the button is activated either with + /// the mouse or the keyboard. + /// + public Action Clicked; + internal Direction FocusDirection { get => SuperView?.FocusDirection ?? focusDirection; set { @@ -389,29 +889,99 @@ namespace Terminal.Gui { /// public View (Rect frame) { + viewText = new ViewText (this); + this.Text = ustring.Empty; + this.Frame = frame; - CanFocus = false; LayoutStyle = LayoutStyle.Absolute; } /// - /// Initializes a new instance of class. + /// Initializes a new instance of using layout. /// /// + /// /// Use , , , and properties to dynamically control the size and location of the view. - /// - /// + /// The will be created using + /// coordinates. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If Height is greater than one, word wrapping is provided. + /// + /// /// This constructor intitalize a View with a of . /// Use , , , and properties to dynamically control the size and location of the view. + /// /// - public View () + public View () : this (text: string.Empty) { } + + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created at the given + /// coordinates with the given string. The size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// No line wrapping is provided. + /// + /// + /// column to locate the Label. + /// row to locate the Label. + /// text to initialize the property with. + public View (int x, int y, ustring text) : this (ViewText.CalcRect (x, y, text), text) { } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created at the given + /// coordinates with the given string. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If rect.Height is greater than one, word wrapping is provided. + /// + /// + /// Location. + /// text to initialize the property with. + public View (Rect rect, ustring text) : this (rect) { + viewText = new ViewText (this); + this.Text = text; + } + + /// + /// Initializes a new instance of using layout. + /// + /// + /// + /// The will be created using + /// coordinates with the given string. The initial size ( will be + /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. + /// + /// + /// If Height is greater than one, word wrapping is provided. + /// + /// + /// text to initialize the property with. + public View (ustring text) : base () + { + viewText = new ViewText (this); + this.Text = text; + CanFocus = false; LayoutStyle = LayoutStyle.Computed; + var r = ViewText.CalcRect (0, 0, text); x = Pos.At (0); y = Pos.At (0); - Height = 0; - Width = 0; + Width = r.Width; + Height = r.Height; } /// @@ -432,6 +1002,7 @@ namespace Terminal.Gui { if (SuperView == null) return; SuperView.SetNeedsLayout (); + viewText.ReFormat (); } /// @@ -819,8 +1390,13 @@ namespace Terminal.Gui { { if (focused != null) focused.PositionCursor (); - else - Move (frame.X, frame.Y); + else { + if (CanFocus && HasFocus) { + Move (viewText.HotKeyPos == -1 ? 1 : viewText.HotKeyPos, 0); + } else { + Move (frame.X, frame.Y); + } + } } /// @@ -971,6 +1547,13 @@ namespace Terminal.Gui { { var clipRect = new Rect (Point.Empty, frame.Size); + Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); + + // Draw any Text + // TODO: Figure out if this should go here or after OnDrawContent + viewText?.ReFormat (); + viewText?.Draw (bounds); + // Invoke DrawContentEvent OnDrawContent (bounds); @@ -1083,7 +1666,6 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - KeyEventEventArgs args = new KeyEventEventArgs (keyEvent); KeyPress?.Invoke (args); if (args.Handled) @@ -1091,6 +1673,12 @@ namespace Terminal.Gui { if (Focused?.ProcessKey (keyEvent) == true) return true; + var c = keyEvent.KeyValue; + if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == HotKey) { + Clicked?.Invoke (); + return true; + } + return false; } @@ -1381,7 +1969,7 @@ namespace Terminal.Gui { } if (edges.Any ()) { - if (!object.ReferenceEquals(edges.First ().From, edges.First ().To)) { + if (!object.ReferenceEquals (edges.First ().From, edges.First ().To)) { throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {edges.First ().From}. Did you forget to add it to {this}?"); } else { throw new InvalidOperationException ("TopologicalSort encountered a recursive cycle in the relative Pos/Dim in the views of " + this); @@ -1430,6 +2018,9 @@ namespace Terminal.Gui { if (!layoutNeeded) return; + viewText.TextSize = Bounds.Size; + viewText.ReFormat (); + Rect oldBounds = Bounds; // Sort out the dependencies of the X, Y, Width, Height properties @@ -1471,146 +2062,40 @@ namespace Terminal.Gui { } /// - /// A generic virtual method at the level of View to manipulate any hot-keys. + /// The text displayed by the . /// - /// The text to manipulate. - /// The hot-key to look for. - /// The returning hot-key position. - /// The character immediately to the right relative to the hot-key position - /// It aims to facilitate the preparation for procedures. - public virtual ustring GetTextFromHotKey (ustring text, Rune hotKey, out int hotPos, out Rune showHotKey) - { - Rune hot_key = (Rune)0; - int hot_pos = -1; - ustring shown_text = text; - - // Use first hot_key char passed into 'hotKey'. - int i = 0; - foreach (Rune c in shown_text) { - if ((char)c != 0xFFFD) { - if (c == hotKey) { - hot_pos = i; - } else if (hot_pos > -1) { - hot_key = c; - break; - } - } - i++; + /// + /// The text will only be displayed if the View has no subviews. + /// + public virtual ustring Text { + get => viewText.Text; + set { + viewText.Text = value; + SetNeedsDisplay (); } - - if (hot_pos == -1) { - // Use first upper-case char if there are no hot-key in the text. - i = 0; - foreach (Rune c in shown_text) { - if ((char)c != 0xFFFD) { - if (Rune.IsUpper (c)) { - hot_key = c; - hot_pos = i; - break; - } - } - i++; - } - } else { - // Use char after 'hotKey' - ustring start = ""; - i = 0; - foreach (Rune c in shown_text) { - start += ustring.Make (c); - i++; - if (i == hot_pos) - break; - } - var st = shown_text; - shown_text = start; - i = 0; - foreach (Rune c in st) { - i++; - if (i > hot_pos + 1) { - shown_text += ustring.Make (c); - } - } - } - hotPos = hot_pos; - showHotKey = hot_key; - return shown_text; } /// - /// A generic virtual method at the level of View to manipulate any hot-keys with process. + /// Controls the text-alignment property of the View. Changing this property will redisplay the . /// - /// The text to manipulate to align. - /// The passed in hot-key position. - /// The returning hot-key position. - /// The to align to. - /// It performs the process to the caller. - public virtual ustring GetTextAlignment (ustring shown_text, int hot_pos, out int c_hot_pos, TextAlignment textAlignment) - { - int start; - var caption = shown_text; - c_hot_pos = hot_pos; - - if (Frame.Width > shown_text.Length + 1) { - switch (textAlignment) { - case TextAlignment.Left: - caption += new string (' ', Frame.Width - caption.RuneCount); - break; - case TextAlignment.Right: - start = Frame.Width - caption.RuneCount; - caption = $"{new string (' ', Frame.Width - caption.RuneCount)}{caption}"; - if (c_hot_pos > -1) { - c_hot_pos += start; - } - break; - case TextAlignment.Centered: - start = Frame.Width / 2 - caption.RuneCount / 2; - caption = $"{new string (' ', start)}{caption}{new string (' ', Frame.Width - caption.RuneCount - start)}"; - if (c_hot_pos > -1) { - c_hot_pos += start; - } - break; - case TextAlignment.Justified: - var words = caption.Split (" "); - var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos); - var space = (Frame.Width - runeCount) / (caption.Length - wLen); - caption = ""; - for (int i = 0; i < words.Length; i++) { - if (i == words.Length - 1) { - caption += new string (' ', Frame.Width - caption.RuneCount - 1); - caption += words [i]; - } else { - caption += words [i]; - } - if (i < words.Length - 1) { - caption += new string (' ', space); - } - } - if (c_hot_pos > -1) { - c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1; - } - break; - } + /// The text alignment. + public virtual TextAlignment TextAlignment { + get => viewText.TextAlignment; + set { + viewText.TextAlignment = value; + SetNeedsDisplay (); } - - return caption; } - int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos) - { - int length = 0; - int rCount = 0; - int wHotPos = -1; - for (int i = 0; i < words.Length; i++) { - if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos) - wHotPos = i; - length += words [i].Length; - rCount += words [i].RuneCount; + /// + /// The color used for the . + /// + public virtual Attribute TextColor { + get => viewText.TextColor; + set { + viewText.TextColor = value; + SetNeedsDisplay (); } - if (wHotPos == -1 && hotPos > -1) - wHotPos = words.Length; - runeCount = rCount; - wordHotPos = wHotPos; - return length; } /// @@ -1682,6 +2167,16 @@ namespace Terminal.Gui { if (MouseEvent (mouseEvent)) return true; + + if (mouseEvent.Flags == MouseFlags.Button1Clicked) { + if (!HasFocus) { + SuperView.SetFocus (this); + SetNeedsDisplay (); + } + + Clicked?.Invoke (); + return true; + } return false; } } diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs index 1cd4be592..d02a6c21f 100644 --- a/Terminal.Gui/Core/Window.cs +++ b/Terminal.Gui/Core/Window.cs @@ -2,6 +2,13 @@ // Authors: // Miguel de Icaza (miguel@gnome.org) // +// NOTE: Window is functionally identical to FrameView with the following exceptions. +// - Window is a Toplevel +// - FrameView Does not support padding (but should) +// - FrameView Does not support mouse dragging +// - FrameView Does not support IEnumerable +// Any udpates done here should probably be done in FrameView as well; TODO: Merge these classes + using System.Collections; using NStack; @@ -29,7 +36,6 @@ namespace Terminal.Gui { } } - /// /// ContentView is an internal implementation detail of Window. It is used to host Views added with . /// Its ONLY reason for being is to provide a simple way for Window to expose to those SubViews that the Window's Bounds @@ -38,21 +44,6 @@ namespace Terminal.Gui { class ContentView : View { public ContentView (Rect frame) : base (frame) { } public ContentView () : base () { } -#if false - public override void Redraw (Rect bounds) - { - Driver.SetAttribute (ColorScheme.Focus); - - for (int y = 0; y < Frame.Height; y++) { - Move (0, y); - for (int x = 0; x < Frame.Width; x++) { - - Driver.AddRune ('x'); - } - } - base.Redraw (region); - } -#endif } /// @@ -263,5 +254,39 @@ namespace Terminal.Gui { //Demo.ml.Text = me.ToString (); return false; } + + /// + /// The text displayed by the . + /// + public override ustring Text { + get => contentView.Text; + set { + base.Text = value; + if (contentView != null) { + contentView.Text = value; + } + } + } + + /// + /// Controls the text-alignment property of the label, changing it will redisplay the . + /// + /// The text alignment. + public override TextAlignment TextAlignment { + get => contentView.TextAlignment; + set { + base.TextAlignment = contentView.TextAlignment = value; + } + } + + /// + /// The color used for the . + /// + public override Attribute TextColor { + get => contentView.TextColor; + set { + base.TextColor = contentView.TextColor = value; + } + } } } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index c542d3dee..e3acf8933 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -26,11 +26,7 @@ namespace Terminal.Gui { /// public class Button : View { ustring text; - ustring shown_text; - Rune hot_key; - int hot_pos = -1; bool is_default; - TextAlignment textAlignment = TextAlignment.Centered; /// /// Gets or sets whether the is the default action to activate in a dialog. @@ -45,16 +41,6 @@ namespace Terminal.Gui { } } - /// - /// Clicked , raised when the button is clicked. - /// - /// - /// Client code can hook up to this event, it is - /// raised when the button is activated either with - /// the mouse or the keyboard. - /// - public Action Clicked; - /// /// Initializes a new instance of using layout. /// @@ -120,6 +106,8 @@ namespace Terminal.Gui { void Init (ustring text, bool is_default) { + HotKeySpecifier = new Rune ('_'); + _leftBracket = new Rune (Driver != null ? Driver.LeftBracket : '['); _rightBracket = new Rune (Driver != null ? Driver.RightBracket : ']'); _leftDefault = new Rune (Driver != null ? Driver.LeftDefaultIndicator : '<'); @@ -144,7 +132,7 @@ namespace Terminal.Gui { /// /// The text displayed by this . /// - public ustring Text { + public new ustring Text { get { return text; } @@ -156,58 +144,19 @@ namespace Terminal.Gui { } } - /// - /// Sets or gets the text alignment for the . - /// - public TextAlignment TextAlignment { - get => textAlignment; - set { - textAlignment = value; - Update (); - } - } - internal void Update () { if (IsDefault) - shown_text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket); + base.Text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket); else - shown_text = ustring.Make (_leftBracket) + " " + text + " " + ustring.Make (_rightBracket); - - shown_text = GetTextFromHotKey (shown_text, '_', out hot_pos, out hot_key); + base.Text = ustring.Make (_leftBracket) + " " + text + " " + ustring.Make (_rightBracket); SetNeedsDisplay (); } - int c_hot_pos; - - /// - public override void Redraw (Rect bounds) - { - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal); - Move (0, 0); - - var caption = GetTextAlignment (shown_text, hot_pos, out int s_hot_pos, TextAlignment); - c_hot_pos = s_hot_pos; - - Driver.AddStr (caption); - - if (c_hot_pos != -1) { - Move (c_hot_pos, 0); - Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal); - Driver.AddRune (hot_key); - } - } - - /// - public override void PositionCursor () - { - Move (c_hot_pos == -1 ? 1 : c_hot_pos, 0); - } - bool CheckKey (KeyEvent key) { - if ((char)key.KeyValue == hot_key) { + if ((char)key.KeyValue == HotKey) { this.SuperView.SetFocus (this); Clicked?.Invoke (); return true; @@ -238,26 +187,12 @@ namespace Terminal.Gui { public override bool ProcessKey (KeyEvent kb) { var c = kb.KeyValue; - if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == hot_key) { + if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == HotKey) { Clicked?.Invoke (); return true; } return base.ProcessKey (kb); } - /// - public override bool MouseEvent (MouseEvent me) - { - if (me.Flags == MouseFlags.Button1Clicked) { - if (!HasFocus) { - SuperView.SetFocus (this); - SetNeedsDisplay (); - } - - Clicked?.Invoke (); - return true; - } - return false; - } } } diff --git a/Terminal.Gui/Views/Checkbox.cs b/Terminal.Gui/Views/Checkbox.cs index 80f550548..ebae828ba 100644 --- a/Terminal.Gui/Views/Checkbox.cs +++ b/Terminal.Gui/Views/Checkbox.cs @@ -88,7 +88,7 @@ namespace Terminal.Gui { /// /// The text displayed by this /// - public ustring Text { + public new ustring Text { get { return text; } diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 99e306412..846d61041 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -270,7 +270,7 @@ namespace Terminal.Gui { /// /// The currently selected list item /// - public ustring Text + public new ustring Text { get { diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index 5b7f49a4f..b9c49f60d 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -1,12 +1,14 @@ // -// FrameView.cs: Frame control -// // Authors: // Miguel de Icaza (miguel@gnome.org) // -using System; -using System.Collections; -using System.Collections.Generic; +// NOTE: FrameView is functionally identical to Window with the following exceptions. +// - Is not a Toplevel +// - Does not support mouse dragging +// - Does not support padding (but should) +// - Does not support IEnumerable +// Any udpates done here should probably be done in Window as well; TODO: Merge these classes + using NStack; namespace Terminal.Gui { @@ -30,6 +32,11 @@ namespace Terminal.Gui { } } + /// + /// ContentView is an internal implementation detail of Window. It is used to host Views added with . + /// Its ONLY reason for being is to provide a simple way for Window to expose to those SubViews that the Window's Bounds + /// are actually deflated due to the border. + /// class ContentView : View { public ContentView (Rect frame) : base (frame) { } public ContentView () : base () { } @@ -40,7 +47,7 @@ namespace Terminal.Gui { /// /// Frame. /// Title. - public FrameView (Rect frame, ustring title) : base (frame) + public FrameView (Rect frame, ustring title = null) : base (frame) { var cFrame = new Rect (1, 1, frame.Width - 2, frame.Height - 2); this.title = title; @@ -86,6 +93,7 @@ namespace Terminal.Gui { void Initialize () { base.Add (contentView); + contentView.Text = base.Text; } void DrawFrame () @@ -158,5 +166,40 @@ namespace Terminal.Gui { Driver.DrawWindowTitle (scrRect, Title, padding, padding, padding, padding); Driver.SetAttribute (ColorScheme.Normal); } + + /// + /// The text displayed by the . + /// + public override ustring Text { + get => contentView.Text; + set { + base.Text = value; + if (contentView != null) { + contentView.Text = value; + } + } + } + + /// + /// Controls the text-alignment property of the label, changing it will redisplay the . + /// + /// The text alignment. + public override TextAlignment TextAlignment { + get => contentView.TextAlignment; + set { + base.TextAlignment = contentView.TextAlignment = value; + } + } + + /// + /// The color used for the . + /// + public override Attribute TextColor { + get => contentView.TextColor; + set { + base.TextColor = contentView.TextColor = value; + } + } + } } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 1bfa7a379..117d74f79 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -15,350 +15,33 @@ namespace Terminal.Gui { /// /// The Label displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap. /// + /// + /// The view is functionality identical to and is included for API backwards compatibilty. + /// public class Label : View { - List lines = new List (); - bool recalcPending = true; - ustring text; - TextAlignment textAlignment; - - static Rect CalcRect (int x, int y, ustring s) - { - int mw = 0; - int ml = 1; - - int cols = 0; - foreach (var rune in s) { - if (rune == '\n') { - ml++; - if (cols > mw) - mw = cols; - cols = 0; - } else - cols++; - } - if (cols > mw) - mw = cols; - - return new Rect (x, y, mw, ml); - } - - /// - /// Initializes a new instance of using layout. - /// - /// - /// - /// The will be created at the given - /// coordinates with the given string. The size ( will be - /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. - /// - /// - /// No line wrapping is provided. - /// - /// - /// column to locate the Label. - /// row to locate the Label. - /// text to initialize the property with. - public Label (int x, int y, ustring text) : this (CalcRect (x, y, text), text) + /// + public Label () { } - /// - /// Initializes a new instance of using layout. - /// - /// - /// - /// The will be created at the given - /// coordinates with the given string. The initial size ( will be - /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. - /// - /// - /// If rect.Height is greater than one, word wrapping is provided. - /// - /// - /// Location. - /// text to initialize the property with. - public Label (Rect rect, ustring text) : base (rect) + /// + public Label (Rect frame) : base (frame) { - this.text = text; } - /// - /// Initializes a new instance of using layout. - /// - /// - /// - /// The will be created using - /// coordinates with the given string. The initial size ( will be - /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. - /// - /// - /// If Height is greater than one, word wrapping is provided. - /// - /// - /// text to initialize the property with. - public Label (ustring text) : base () + /// + public Label (ustring text) : base (text) { - this.text = text; - var r = CalcRect (0, 0, text); - Width = r.Width; - Height = r.Height; } - /// - /// Initializes a new instance of using layout. - /// - /// - /// - /// The will be created using - /// coordinates. The initial size ( will be - /// adjusted to fit the contents of , including newlines ('\n') for multiple lines. - /// - /// - /// If Height is greater than one, word wrapping is provided. - /// - /// - public Label () : this (text: string.Empty) { } - - static char [] whitespace = new char [] { ' ', '\t' }; - - static ustring ClipAndJustify (ustring str, int width, TextAlignment talign) + /// + public Label (Rect rect, ustring text) : base (rect, text) { - int slen = str.RuneCount; - if (slen > width) { - var uints = str.ToRunes (width); - var runes = new Rune [uints.Length]; - for (int i = 0; i < uints.Length; i++) - runes [i] = uints [i]; - return ustring.Make (runes); - } else { - if (talign == TextAlignment.Justified) { - // TODO: ustring needs this - var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries); - int textCount = words.Sum (arg => arg.Length); - - var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0; - var extras = words.Length > 1 ? (width - textCount) % words.Length : 0; - - var s = new System.Text.StringBuilder (); - //s.Append ($"tc={textCount} sp={spaces},x={extras} - "); - for (int w = 0; w < words.Length; w++) { - var x = words [w]; - s.Append (x); - if (w + 1 < words.Length) - for (int i = 0; i < spaces; i++) - s.Append (' '); - if (extras > 0) { - //s.Append ('_'); - extras--; - } - } - return ustring.Make (s.ToString ()); - } - return str; - } } - void Recalc () + /// + public Label (int x, int y, ustring text) : base (x, y, text) { - recalcPending = false; - Recalc (text, lines, Frame.Width, textAlignment, Bounds.Height > 1); - } - - static ustring StripCRLF (ustring str) - { - var runes = new List (); - foreach (var r in str.ToRunes ()) { - if (r != '\r' && r != '\n') { - runes.Add (r); - } - } - return ustring.Make (runes); ; - } - static ustring ReplaceCRLFWithSpace (ustring str) - { - var runes = new List (); - foreach (var r in str.ToRunes ()) { - if (r == '\r' || r == '\n') { - runes.Add (new Rune (' ')); // r + 0x2400)); // U+25A1 □ WHITE SQUARE - } else { - runes.Add (r); - } - } - return ustring.Make (runes); ; - } - - static List WordWrap (ustring text, int margin) - { - int start = 0, end; - var lines = new List (); - - text = StripCRLF (text); - - while ((end = start + margin) < text.Length) { - while (text [end] != ' ' && end > start) - end -= 1; - - if (end == start) - end = start + margin; - - lines.Add (text [start, end]); - start = end + 1; - } - - if (start < text.Length) - lines.Add (text.Substring (start)); - - return lines; - } - - static void Recalc (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap) - { - lineResult.Clear (); - - if (wordWrap == false) { - textStr = ReplaceCRLFWithSpace (textStr); - lineResult.Add (ClipAndJustify (textStr, width, talign)); - return; - } - - int textLen = textStr.Length; - int lp = 0; - for (int i = 0; i < textLen; i++) { - Rune c = textStr [i]; - if (c == '\n') { - var wrappedLines = WordWrap (textStr [lp, i], width); - foreach (var line in wrappedLines) { - lineResult.Add (ClipAndJustify (line, width, talign)); - } - if (wrappedLines.Count == 0) { - lineResult.Add (ustring.Empty); - } - lp = i + 1; - } - } - foreach (var line in WordWrap (textStr [lp, textLen], width)) { - lineResult.Add (ClipAndJustify (line, width, talign)); - } - } - - /// - public override void LayoutSubviews () - { - recalcPending = true; - } - - /// - public override void Redraw (Rect bounds) - { - if (recalcPending) - Recalc (); - - if (TextColor != -1) - Driver.SetAttribute (TextColor); - else - Driver.SetAttribute (ColorScheme.Normal); - - Clear (); - for (int line = 0; line < lines.Count; line++) { - if (line < bounds.Top || line >= bounds.Bottom) - continue; - var str = lines [line]; - int x; - switch (textAlignment) { - case TextAlignment.Left: - x = 0; - break; - case TextAlignment.Justified: - x = Bounds.Left; - break; - case TextAlignment.Right: - x = Bounds.Right - str.Length; - break; - case TextAlignment.Centered: - x = Bounds.Left + (Bounds.Width - str.Length) / 2; - break; - default: - throw new ArgumentOutOfRangeException (); - } - Move (x, line); - Driver.AddStr (str); - } - } - - /// - /// Computes the number of lines needed to render the specified text by the view - /// - /// Number of lines. - /// Text, may contain newlines. - /// The width for the text. - public static int MeasureLines (ustring text, int width) - { - var result = new List (); - Recalc (text, result, width, TextAlignment.Left, true); - return result.Count; - } - - /// - /// Computes the max width of a line or multilines needed to render by the Label control - /// - /// Max width of lines. - /// Text, may contain newlines. - /// The width for the text. - public static int MaxWidth (ustring text, int width) - { - var result = new List (); - Recalc (text, result, width, TextAlignment.Left, true); - return result.Max (s => s.RuneCount); - } - - /// - /// Computes the max height of a line or multilines needed to render by the Label control - /// - /// Max height of lines. - /// Text, may contain newlines. - /// The width for the text. - public static int MaxHeight (ustring text, int width) - { - var result = new List (); - Recalc (text, result, width, TextAlignment.Left, true); - return result.Count; - } - - /// - /// The text displayed by the . - /// - public virtual ustring Text { - get => text; - set { - text = value; - recalcPending = true; - SetNeedsDisplay (); - } - } - - /// - /// Controls the text-alignment property of the label, changing it will redisplay the . - /// - /// The text alignment. - public TextAlignment TextAlignment { - get => textAlignment; - set { - textAlignment = value; - SetNeedsDisplay (); - } - } - - Attribute textColor = -1; - /// - /// The color used for the . - /// - public Attribute TextColor { - get => textColor; - set { - textColor = value; - SetNeedsDisplay (); - } } } - } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 7bcd7aaee..50db7b3e2 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -144,7 +144,6 @@ namespace Terminal.Gui { Driver.AddStr (ustring.Make(new Rune[] { (i == selected ? Driver.Selected : Driver.UnSelected), ' '})); DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme); } - base.Redraw (bounds); } /// diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 6f709af3b..82d93a178 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -63,7 +63,8 @@ namespace Terminal.Gui { /// Initial text contents. public TextField (ustring text) { - Initialize (text, Frame.Width); + Initialize (text, 0); + Width = text.Length + 1; } /// @@ -122,7 +123,7 @@ namespace Terminal.Gui { /// /// /// - public ustring Text { + public new ustring Text { get { return ustring.Make (text); } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index b9a09527d..f1d40a9cf 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -326,7 +326,7 @@ namespace Terminal.Gui { /// /// /// - public ustring Text { + public override ustring Text { get { return model.ToString (); } diff --git a/Terminal.Gui/Windows/FileDialog.cs b/Terminal.Gui/Windows/FileDialog.cs index 974cfd61a..684565e29 100644 --- a/Terminal.Gui/Windows/FileDialog.cs +++ b/Terminal.Gui/Windows/FileDialog.cs @@ -440,7 +440,7 @@ namespace Terminal.Gui { public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title, Driver.Cols - 20, Driver.Rows - 5, null) { this.message = new Label (Rect.Empty, "MESSAGE" + message); - var msgLines = Label.MeasureLines (message, Driver.Cols - 20); + var msgLines = ViewText.MaxLines (message, Driver.Cols - 20); dirLabel = new Label ("Directory: ") { X = 1, diff --git a/Terminal.Gui/Windows/MessageBox.cs b/Terminal.Gui/Windows/MessageBox.cs index ae1834749..70122e934 100644 --- a/Terminal.Gui/Windows/MessageBox.cs +++ b/Terminal.Gui/Windows/MessageBox.cs @@ -94,8 +94,8 @@ namespace Terminal.Gui { static int QueryFull (bool useErrorColors, int width, int height, ustring title, ustring message, params ustring [] buttons) { const int defaultWidth = 50; - int textWidth = Label.MaxWidth (message, width); - int textHeight = Label.MaxHeight (message, width == 0 ? defaultWidth : width); // message.Count (ustring.Make ('\n')) + 1; + int textWidth = View.ViewText.MaxWidth (message, width == 0 ? defaultWidth : width); + int textHeight = View.ViewText.MaxLines (message, textWidth); // message.Count (ustring.Make ('\n')) + 1; int msgboxHeight = Math.Max (1, textHeight) + 3; // textHeight + (top + top padding + buttons + bottom) // Create button array for Dialog diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index a875c4658..bf6efadb1 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -24,7 +24,8 @@ namespace UICatalog { Width = CharMap.RowWidth + 2, Height = Dim.Fill (), Start = 0x2500, - ColorScheme = Colors.Dialog + ColorScheme = Colors.Dialog, + CanFocus = true, }; Win.Add (charMap); diff --git a/UICatalog/Scenarios/LabelsAsButtons.cs b/UICatalog/Scenarios/LabelsAsButtons.cs new file mode 100644 index 000000000..60351a34d --- /dev/null +++ b/UICatalog/Scenarios/LabelsAsButtons.cs @@ -0,0 +1,271 @@ +using NStack; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "LabelsAsButtons", Description: "POC to see how making Label more a base class would work")] + [ScenarioCategory ("Controls")] + [ScenarioCategory ("POC")] + class LabelsAsButtons : Scenario { + public override void Setup () + { + // Add a label & text field so we can demo IsDefault + var editLabel = new Label ("TextField (to demo IsDefault):") { + X = 0, + Y = 0, + }; + Win.Add (editLabel); + // Add a TextField using Absolute layout. + var edit = new TextField (31, 0, 15, ""); + Win.Add (edit); + + // This is the default button (IsDefault = true); if user presses ENTER in the TextField + // the scenario will quit + var defaultButton = new Label ("_Quit") { + X = Pos.Center (), + //TODO: Change to use Pos.AnchorEnd() + Y = Pos.Bottom (Win) - 3, + //IsDefault = true, + Clicked = () => Application.RequestStop (), + }; + Win.Add (defaultButton); + + var swapButton = new Label (50, 0, "Swap Default (Absolute Layout)"); + swapButton.Clicked = () => { + //defaultButton.IsDefault = !defaultButton.IsDefault; + //swapButton.IsDefault = !swapButton.IsDefault; + }; + Win.Add (swapButton); + + static void DoMessage (Label button, ustring txt) + { + button.Clicked = () => { + var btnText = button.Text.ToString (); + MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No"); + }; + } + + var colorButtonsLabel = new Label ("Color Buttons:") { + X = 0, + Y = Pos.Bottom (editLabel) + 1, + }; + Win.Add (colorButtonsLabel); + + //View prev = colorButtonsLabel; + + //With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds); + var x = Pos.Right (colorButtonsLabel) + 2; + foreach (var colorScheme in Colors.ColorSchemes) { + var colorButton = new Label ($"{colorScheme.Key}") { + ColorScheme = colorScheme.Value, + //X = Pos.Right (prev) + 2, + X = x, + Y = Pos.Y (colorButtonsLabel), + }; + DoMessage (colorButton, colorButton.Text); + Win.Add (colorButton); + //prev = colorButton; + x += colorButton.Frame.Width + 2; + } + // BUGBUG: For some reason these buttons don't move to correct locations initially. + // This was the only way I find to resolves this with the View prev variable. + //Top.Ready += () => Top.Redraw (Top.Bounds); + + Label button; + Win.Add (button = new Label ("A super long _Button that will probably expose a bug in clipping or wrapping of text. Will it?") { + X = 2, + Y = Pos.Bottom (colorButtonsLabel) + 1, + }); + DoMessage (button, button.Text); + + // Note the 'N' in 'Newline' will be the hotkey + Win.Add (button = new Label ("a Newline\nin the button") { + X = 2, + Y = Pos.Bottom (button) + 1, + Clicked = () => MessageBox.Query ("Message", "Question?", "Yes", "No") + }); + + var textChanger = new Label ("Te_xt Changer") { + X = 2, + Y = Pos.Bottom (button) + 1, + }; + Win.Add (textChanger); + textChanger.Clicked = () => textChanger.Text += "!"; + + Win.Add (button = new Label ("Lets see if this will move as \"Text Changer\" grows") { + X = Pos.Right (textChanger) + 2, + Y = Pos.Y (textChanger), + }); + + var removeButton = new Label ("Remove this button") { + X = 2, + Y = Pos.Bottom (button) + 1, + ColorScheme = Colors.Error + }; + Win.Add (removeButton); + // This in intresting test case because `moveBtn` and below are laid out relative to this one! + removeButton.Clicked = () => Win.Remove (removeButton); + + var computedFrame = new FrameView ("Computed Layout") { + X = 0, + Y = Pos.Bottom (removeButton) + 1, + Width = Dim.Percent (50), + Height = 5 + }; + Win.Add (computedFrame); + + // Demonstrates how changing the View.Frame property can move Views + var moveBtn = new Label ("Move This \u263b Button _via Pos") { + X = 0, + Y = Pos.Center () - 1, + Width = 30, + ColorScheme = Colors.Error, + }; + moveBtn.Clicked = () => { + moveBtn.X = moveBtn.Frame.X + 5; + // This is already fixed with the call to SetNeedDisplay() in the Pos Dim. + //computedFrame.LayoutSubviews (); // BUGBUG: This call should not be needed. View.X is not causing relayout correctly + }; + computedFrame.Add (moveBtn); + + // Demonstrates how changing the View.Frame property can SIZE Views (#583) + var sizeBtn = new Label ("Size This \u263a Button _via Pos") { + X = 0, + Y = Pos.Center () + 1, + Width = 30, + ColorScheme = Colors.Error, + }; + sizeBtn.Clicked = () => { + sizeBtn.Width = sizeBtn.Frame.Width + 5; + //computedFrame.LayoutSubviews (); // FIXED: This call should not be needed. View.X is not causing relayout correctly + }; + computedFrame.Add (sizeBtn); + + var absoluteFrame = new FrameView ("Absolute Layout") { + X = Pos.Right (computedFrame), + Y = Pos.Bottom (removeButton) + 1, + Width = Dim.Fill (), + Height = 5 + }; + Win.Add (absoluteFrame); + + // Demonstrates how changing the View.Frame property can move Views + var moveBtnA = new Label (0, 0, "Move This Button via Frame") { + ColorScheme = Colors.Error, + }; + moveBtnA.Clicked = () => { + moveBtnA.Frame = new Rect (moveBtnA.Frame.X + 5, moveBtnA.Frame.Y, moveBtnA.Frame.Width, moveBtnA.Frame.Height); + }; + absoluteFrame.Add (moveBtnA); + + // Demonstrates how changing the View.Frame property can SIZE Views (#583) + var sizeBtnA = new Label (0, 2, " ~  s  gui.cs   master ↑10 = Со_хранить") { + ColorScheme = Colors.Error, + }; + sizeBtnA.Clicked = () => { + sizeBtnA.Frame = new Rect (sizeBtnA.Frame.X, sizeBtnA.Frame.Y, sizeBtnA.Frame.Width + 5, sizeBtnA.Frame.Height); + }; + absoluteFrame.Add (sizeBtnA); + + var label = new Label ("Text Alignment (changes the four buttons above): ") { + X = 2, + Y = Pos.Bottom (computedFrame) + 1, + }; + Win.Add (label); + + var radioGroup = new RadioGroup (new ustring [] { "Left", "Right", "Centered", "Justified" }) { + X = 4, + Y = Pos.Bottom (label) + 1, + SelectedItem = 2, + }; + Win.Add (radioGroup); + + // Demo changing hotkey + ustring MoveHotkey (ustring txt) + { + // Remove the '_' + var i = txt.IndexOf ('_'); + ustring start = ""; + if (i > -1) + start = txt [0, i]; + txt = start + txt [i + 1, txt.Length]; + + // Move over one or go to start + i++; + if (i >= txt.Length) { + i = 0; + } + + // Slip in the '_' + start = txt [0, i]; + txt = start + ustring.Make ('_') + txt [i, txt.Length]; + + return txt; + } + + var mhkb = "Click to Change th_is Button's Hotkey"; + var moveHotKeyBtn = new Label (mhkb) { + X = 2, + Y = Pos.Bottom (radioGroup) + 1, + Width = mhkb.Length + 10, + ColorScheme = Colors.TopLevel, + }; + moveHotKeyBtn.Clicked = () => { + moveHotKeyBtn.Text = MoveHotkey (moveHotKeyBtn.Text); + }; + Win.Add (moveHotKeyBtn); + + var muhkb = " ~  s  gui.cs   master ↑10 = Сохранить"; + var moveUnicodeHotKeyBtn = new Label (muhkb) { + X = Pos.Left (absoluteFrame) + 1, + Y = Pos.Bottom (radioGroup) + 1, + Width = muhkb.Length + 30, + ColorScheme = Colors.TopLevel, + }; + moveUnicodeHotKeyBtn.Clicked = () => { + moveUnicodeHotKeyBtn.Text = MoveHotkey (moveUnicodeHotKeyBtn.Text); + }; + Win.Add (moveUnicodeHotKeyBtn); + + radioGroup.SelectedItemChanged += (args) => { + switch (args.SelectedItem) { + case 0: + moveBtn.TextAlignment = TextAlignment.Left; + sizeBtn.TextAlignment = TextAlignment.Left; + moveBtnA.TextAlignment = TextAlignment.Left; + sizeBtnA.TextAlignment = TextAlignment.Left; + moveHotKeyBtn.TextAlignment = TextAlignment.Left; + moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Left; + break; + case 1: + moveBtn.TextAlignment = TextAlignment.Right; + sizeBtn.TextAlignment = TextAlignment.Right; + moveBtnA.TextAlignment = TextAlignment.Right; + sizeBtnA.TextAlignment = TextAlignment.Right; + moveHotKeyBtn.TextAlignment = TextAlignment.Right; + moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Right; + break; + case 2: + moveBtn.TextAlignment = TextAlignment.Centered; + sizeBtn.TextAlignment = TextAlignment.Centered; + moveBtnA.TextAlignment = TextAlignment.Centered; + sizeBtnA.TextAlignment = TextAlignment.Centered; + moveHotKeyBtn.TextAlignment = TextAlignment.Centered; + moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Centered; + break; + case 3: + moveBtn.TextAlignment = TextAlignment.Justified; + sizeBtn.TextAlignment = TextAlignment.Justified; + moveBtnA.TextAlignment = TextAlignment.Justified; + sizeBtnA.TextAlignment = TextAlignment.Justified; + moveHotKeyBtn.TextAlignment = TextAlignment.Justified; + moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Justified; + break; + } + }; + } + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/MessageBoxes.cs b/UICatalog/Scenarios/MessageBoxes.cs index 8075ba51a..5f41537ca 100644 --- a/UICatalog/Scenarios/MessageBoxes.cs +++ b/UICatalog/Scenarios/MessageBoxes.cs @@ -142,7 +142,7 @@ namespace UICatalog { ColorScheme = Colors.Error, }; - var btnText = new [] { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }; + var btnText = new [] { "_Zero", "_One", "T_wo", "_Three", "_Four", "Fi_ve", "Si_x", "_Seven", "_Eight", "_Nine" }; var showMessageBoxButton = new Button ("Show MessageBox") { X = Pos.Center(), diff --git a/UICatalog/Scenarios/ViewWithText.cs b/UICatalog/Scenarios/ViewWithText.cs new file mode 100644 index 000000000..fd88e9922 --- /dev/null +++ b/UICatalog/Scenarios/ViewWithText.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "View Text", Description: "Demos and tests View's Text capabilities.")] + [ScenarioCategory ("Text")] + [ScenarioCategory ("POC")] + class ViewWithText : Scenario { + public override void Setup () + { + Win.Text = "This is the Te_xt for the host Win object. TextAlignment.Centered was specified. It is intentionally very long to illustrate word wrap.\n" + + "<-- There is a new line here to show a hard line break. You should see this text bleed underneath the subviews, which start at Y = 3."; + Win.TextAlignment = TextAlignment.Centered; + Win.TextColor = Application.Driver.MakeAttribute (Color.BrightGreen, Color.Black); +#if true + string txt = "Hello world, how are you today? Pretty neat!"; +#else + string txt = "Hello world, how are you today? Unicode:  ~  gui.cs  . Neat?"; +#endif + var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast ().ToList (); + var label = new View ($"Demonstrating single-line (should clip!):") { Y = 3 }; + Win.Add (label); + foreach (var alignment in alignments) { + label = new Label ($"{alignment}:") { Y = Pos.Bottom (label) }; + Win.Add (label); + label = new Label (txt) { + TextAlignment = alignment, + Y = Pos.Bottom (label), + Width = Dim.Fill (), + Height = 1, + ColorScheme = Colors.Dialog, + TextColor = Application.Driver.MakeAttribute (Color.BrightRed, Color.White), + }; + Win.Add (label); + } + + txt += "\nSecond line\n\nFourth Line."; + label = new View ($"Demonstrating multi-line and word wrap:") { Y = Pos.Bottom (label) + 1 }; + Win.Add (label); + foreach (var alignment in alignments) { + label = new View ($"{alignment}:") { Y = Pos.Bottom (label) }; + Win.Add (label); + label = new View (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = 6, ColorScheme = Colors.Dialog, Y = Pos.Bottom (label) }; + Win.Add (label); + } + } + } +} \ No newline at end of file diff --git a/UICatalog/Scenarios/WindowsAndFrameViews.cs b/UICatalog/Scenarios/WindowsAndFrameViews.cs index 03b23a639..24264146b 100644 --- a/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -64,11 +64,11 @@ namespace UICatalog { X = Pos.Center (), Y = 0, ColorScheme = Colors.Error, - Clicked = () => About() + Clicked = () => About () }); Win.Add (new Button ("Press ME! (Y = Pos.AnchorEnd(1))") { X = Pos.Center (), - Y = Pos.AnchorEnd(1), + Y = Pos.AnchorEnd (1), ColorScheme = Colors.Error }); Top.Add (Win); @@ -80,7 +80,7 @@ namespace UICatalog { X = margin, Y = Pos.Bottom (listWin.Last ()) + (margin), Width = Dim.Fill (margin), - Height = contentHeight + (i*2) + 2, + Height = contentHeight + (i * 2) + 2, }; win.ColorScheme = Colors.Dialog; win.Add (new Button ("Press me! (Y = 0)") { @@ -95,8 +95,10 @@ namespace UICatalog { Width = Dim.Percent (50), Height = 5, ColorScheme = Colors.Base, + Text = "The Text in the Window", }; - subWin.Add (new TextField (win.Title.ToString ()) { + subWin.Add (new TextField ("Edit me! " + win.Title.ToString ()) { + Y = 1, ColorScheme = Colors.Error }); win.Add (subWin); @@ -106,8 +108,12 @@ namespace UICatalog { Width = Dim.Percent (100), Height = 5, ColorScheme = Colors.Base, + Text = "The Text in the FrameView", + }; - frameView.Add (new TextField ("Edit Me")); + frameView.Add (new TextField ("Edit Me!") { + Y = 1, + }); win.Add (frameView); Top.Add (win); @@ -135,6 +141,7 @@ namespace UICatalog { Width = Dim.Percent (50), Height = Dim.Fill () - 1, ColorScheme = Colors.Base, + Text = "The Text in the Window", }; subWinofFV.Add (new TextField ("Edit Me") { ColorScheme = Colors.Error @@ -150,8 +157,9 @@ namespace UICatalog { Width = Dim.Percent (100), Height = Dim.Fill () - 1, ColorScheme = Colors.Base, + Text = "The Text in the FrameView", }; - subFrameViewofFV.Add (new TextField ("Edit Me")); + subFrameViewofFV.Add (new TextField (0, 0, 15, "Edit Me")); subFrameViewofFV.Add (new CheckBox (0, 1, "Check me")); // BUGBUG: This checkbox is not shown even though frameViewFV has 3 rows in @@ -160,12 +168,12 @@ namespace UICatalog { frame.Add (new CheckBox ("Btn1 (Y = Pos.AnchorEnd (1))") { X = 0, - Y = Pos.AnchorEnd (1), + Y = Pos.AnchorEnd (1), }); CheckBox c = new CheckBox ("Btn2 (Y = Pos.AnchorEnd (1))") { - Y = Pos.AnchorEnd (1), + Y = Pos.AnchorEnd (1), }; - c.X = Pos.AnchorEnd () - (Pos.Right (c) - Pos.Left (c)); + c.X = Pos.AnchorEnd () - (Pos.Right (c) - Pos.Left (c)); frame.Add (c); frame.Add (subFrameViewofFV); diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index f89cbf6d3..43473921b 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -25,7 +25,7 @@ namespace Terminal.Gui { Assert.Equal (new Rect (0, 0, 0, 0), r.Frame); Assert.Null (r.Focused); Assert.Null (r.ColorScheme); - Assert.Equal (Dim.Sized (0), r.Height); + Assert.Equal (Dim.Sized (0), r.Width); Assert.Equal (Dim.Sized (0), r.Height); // BUGBUG: Pos needs eqality implemented //Assert.Equal (Pos.At (0), r.X);