diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 26eb08072..e4a73a928 100644 --- a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -85,7 +85,7 @@ public class CharacterMap : Scenario X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 17, - Caption = "e.g. 01BE3 or ✈" + Title = "e.g. 01BE3 or ✈" //SchemeName = "Dialog" }; diff --git a/Examples/UICatalog/Scenarios/TextInputControls.cs b/Examples/UICatalog/Scenarios/TextInputControls.cs index 797d83f2f..19e71a427 100644 --- a/Examples/UICatalog/Scenarios/TextInputControls.cs +++ b/Examples/UICatalog/Scenarios/TextInputControls.cs @@ -68,7 +68,7 @@ public class TextInputControls : Scenario X = Pos.Right (label) + 1, Y = Pos.Bottom (textField), Width = Dim.Percent (50) - 1, - Caption = "TextField with caption" + Title = "TextField with caption" }; win.Add (textField); diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index ac857b4e6..5d4d4b1cb 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -628,7 +628,7 @@ namespace Terminal.Gui.Resources { } /// - /// Looks up a localized string similar to Enter Path. + /// Looks up a localized string similar to _Enter Path. /// internal static string fdPathCaption { get { @@ -682,7 +682,7 @@ namespace Terminal.Gui.Resources { } /// - /// Looks up a localized string similar to Find. + /// Looks up a localized string similar to _Find. /// internal static string fdSearchCaption { get { diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index a05bc2eb5..f7e3edc49 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -192,10 +192,7 @@ Modified - Enter Path - - - Find + _Enter Path Size @@ -359,4 +356,7 @@ _Tree Show/Hide Tree View + + _Find + \ No newline at end of file diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs index 10c3cd99f..0c3a741ac 100644 --- a/Terminal.Gui/ViewBase/View.Command.cs +++ b/Terminal.Gui/ViewBase/View.Command.cs @@ -22,9 +22,9 @@ public partial class View // Command APIs // HotKey - SetFocus and raise HandlingHotKey AddCommand ( Command.HotKey, - () => + (ctx) => { - if (RaiseHandlingHotKey () is true) + if (RaiseHandlingHotKey (ctx) is true) { return true; } @@ -257,15 +257,16 @@ public partial class View // Command APIs /// which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// + /// The context to pass with the command. /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// - protected bool? RaiseHandlingHotKey () + protected bool? RaiseHandlingHotKey (ICommandContext? ctx) { - CommandEventArgs args = new () { Context = new CommandContext { Command = Command.HotKey } }; + CommandEventArgs args = new () { Context = ctx }; //Logging.Debug ($"{Title} ({args.Context?.Source?.Title})"); // Best practice is to invoke the virtual method first. diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index b6165186b..195428988 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -32,7 +32,7 @@ public class CheckBox : View // Hotkey - Advance state and raise Select event - DO NOT raise Accept AddCommand (Command.HotKey, ctx => { - if (RaiseHandlingHotKey () is true) + if (RaiseHandlingHotKey (ctx) is true) { return true; } diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 7fd813fb8..90982e96f 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -148,7 +148,7 @@ public class FileDialog : Dialog, IDesignable e.Handled = true; }; - _tbPath = new () { Width = Dim.Fill (),/* CaptionColor = new (Color.Black)*/ }; + _tbPath = new () { Width = Dim.Fill () }; _tbPath.KeyDown += (s, k) => { @@ -248,7 +248,6 @@ public class FileDialog : Dialog, IDesignable X = 0, Width = Dim.Fill (), Y = Pos.AnchorEnd (), - HotKey = Key.F.WithAlt, Id = "_tbFind", }; @@ -456,8 +455,8 @@ public class FileDialog : Dialog, IDesignable _btnBack.Text = GetBackButtonText (); _btnForward.Text = GetForwardButtonText (); - _tbPath.Caption = Style.PathCaption; - _tbFind.Caption = Style.SearchCaption; + _tbPath.Title = Style.PathCaption; + _tbFind.Title = Style.SearchCaption; _tbPath.Autocomplete.Scheme = new (_tbPath.GetScheme ()) { diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index d315fe502..5ea22dba9 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -60,7 +60,7 @@ public class Label : View, IDesignable private bool? InvokeHotKeyOnNextPeer (ICommandContext commandContext) { - if (RaiseHandlingHotKey () == true) + if (RaiseHandlingHotKey (commandContext) == true) { return true; } diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 1385aa24e..fe2942a96 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -31,11 +31,11 @@ public class MenuBarv2 : Menuv2, IDesignable AddCommand ( Command.HotKey, - () => + (ctx) => { // Logging.Debug ($"{Title} - Command.HotKey"); - if (RaiseHandlingHotKey () is true) + if (RaiseHandlingHotKey (ctx) is true) { return true; } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 1040c6e83..55d52bb51 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -94,7 +94,7 @@ public class RadioGroup : View, IDesignable, IOrientation return false; } - if (RaiseHandlingHotKey () == true) + if (RaiseHandlingHotKey (ctx) == true) { return true; } diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 70d7e007e..bc2390fb6 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -28,9 +28,6 @@ public class TextField : View, IDesignable _selectedStart = -1; _text = new (); - // TODO: Determine if this is a good choice. Previously this was hard coded to - // TODO: DarkGray which was NOT a good choice. - CaptionColor = GetAttributeForRole (VisualRole.Normal).Foreground.GetBrighterColor(); ReadOnly = false; Autocomplete = new TextFieldAutocomplete (); Height = Dim.Auto (DimAutoStyle.Text, 1); @@ -40,9 +37,6 @@ public class TextField : View, IDesignable Used = true; WantMousePositionReports = true; - // By default, disable hotkeys (in case someome sets Title) - HotKeySpecifier = new ('\xffff'); - _historyText.ChangeText += HistoryText_ChangeText; Initialized += TextField_Initialized; @@ -324,6 +318,30 @@ public class TextField : View, IDesignable } ); + AddCommand ( + Command.HotKey, + ctx => + { + if (RaiseHandlingHotKey (ctx) is true) + { + return true; + } + + // If we have focus, then ignore the hotkey because the user + // means to enter it + if (HasFocus) + { + return false; + } + + // This is what the default HotKey handler does: + SetFocus (); + + // Always return true on hotkey, even if SetFocus fails because + // hotkeys are always handled by the View (unless RaiseHandlingHotKey cancels). + return true; + }); + // Default keybindings for this view // We follow this as closely as possible: https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts KeyBindings.Add (Key.Delete, Command.DeleteCharRight); @@ -411,15 +429,6 @@ public class TextField : View, IDesignable /// public IAutocomplete Autocomplete { get; set; } - /// - /// Gets or sets the text to render in control when no value has been entered yet and the does - /// not yet have input focus. - /// - public string Caption { get; set; } - - /// Gets or sets the foreground to use when rendering . - public Color CaptionColor { get; set; } - /// Get the Context Menu for this view. [CanBeNull] public PopoverMenu ContextMenu { get; private set; } @@ -920,7 +929,7 @@ public class TextField : View, IDesignable _isDrawing = true; // Cache attributes as GetAttributeForRole might raise events - Attribute selectedAttribute = new Attribute (GetAttributeForRole (VisualRole.Active)); + var selectedAttribute = new Attribute (GetAttributeForRole (VisualRole.Active)); Attribute readonlyAttribute = GetAttributeForRole (VisualRole.ReadOnly); Attribute normalAttribute = GetAttributeForRole (VisualRole.Editable); @@ -943,7 +952,7 @@ public class TextField : View, IDesignable { // Disabled SetAttributeForRole (VisualRole.Disabled); - } + } else if (idx == _cursorPosition && HasFocus && !Used && SelectedLength == 0 && !ReadOnly) { // Selected text @@ -1157,7 +1166,6 @@ public class TextField : View, IDesignable ///// //public event EventHandler> TextChanged; - /// Undoes the latest changes. public void Undo () { @@ -1699,25 +1707,33 @@ public class TextField : View, IDesignable private void RenderCaption () { if (HasFocus - || Caption == null - || Caption.Length == 0 + || string.IsNullOrEmpty (Title) || Text?.Length > 0) { return; } - var color = new Attribute (CaptionColor, GetAttributeForRole (VisualRole.Editable).Background, GetAttributeForRole (VisualRole.Editable).Style); - SetAttribute (color); - - Move (0, 0); - string render = Caption; - - if (render.GetColumns () > Viewport.Width) + // Ensure TitleTextFormatter has the current Title text + // (should already be set by the Title property setter, but being defensive) + if (TitleTextFormatter.Text != Title) { - render = render [..Viewport.Width]; + TitleTextFormatter.Text = Title; } - AddStr (render); + var captionAttribute = new Attribute ( + GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + GetAttributeForRole (VisualRole.Editable).Background); + + var hotKeyAttribute = new Attribute ( + GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + GetAttributeForRole (VisualRole.Editable).Background, + GetAttributeForRole (VisualRole.Editable).Style | TextStyle.Underline); + + // Use TitleTextFormatter to render the caption with hotkey support + TitleTextFormatter.Draw ( + ViewportToScreen (new Rectangle (0, 0, Viewport.Width, 1)), + captionAttribute, + hotKeyAttribute); } private void SetClipboard (IEnumerable text) @@ -1814,11 +1830,11 @@ public class TextField : View, IDesignable } } - /// + /// public bool EnableForDesign () { Text = "This is a test."; - Caption = "Caption"; + Title = "Caption"; return true; } diff --git a/Tests/UnitTests/FileServices/FileDialogTests.cs b/Tests/UnitTests/FileServices/FileDialogTests.cs index 6a7840096..9488884eb 100644 --- a/Tests/UnitTests/FileServices/FileDialogTests.cs +++ b/Tests/UnitTests/FileServices/FileDialogTests.cs @@ -107,7 +107,7 @@ public class FileDialogTests () Assert.IsType (dlg.MostFocused); Assert.Same (tf, dlg.MostFocused); - Assert.Equal ("Find", tf.Caption); + Assert.Equal ("_Find", tf.Title); // Dialog has not yet been confirmed with a choice Assert.True (dlg.Canceled); diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 40fc6a300..123201f49 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -145,7 +145,7 @@ public class TextFieldTests (ITestOutputHelper output) TextField tf = GetTextFieldsInView (); // Caption has no effect when focused - tf.Caption = caption; + tf.Title = caption; Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); @@ -165,7 +165,7 @@ public class TextFieldTests (ITestOutputHelper output) TextField tf = GetTextFieldsInView (); - tf.Caption = caption; + tf.Title = caption; Application.RaiseKeyDownEvent ('\t'); Assert.False (tf.HasFocus); @@ -185,7 +185,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("", output); - tf.Caption = "Enter txt"; + tf.Title = "Enter txt"; Application.RaiseKeyDownEvent ('\t'); // Caption should appear when not focused and no text @@ -212,7 +212,7 @@ public class TextFieldTests (ITestOutputHelper output) DriverAssert.AssertDriverContentsAre ("", output); // Caption has no effect when focused - tf.Caption = "Enter txt"; + tf.Title = "Enter txt"; Assert.True (tf.HasFocus); View.SetClipToScreen (); tf.Draw (); @@ -227,6 +227,104 @@ public class TextFieldTests (ITestOutputHelper output) Application.Top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void Title_RendersAsCaption_WithCorrectAttributes () + { + TextField tf = GetTextFieldsInView (); + + // Set a title (caption) + tf.Title = "Enter text"; + + // Remove focus so caption appears + Application.RaiseKeyDownEvent ('\t'); + Assert.False (tf.HasFocus); + + View.SetClipToScreen (); + tf.Draw (); + + // Verify the caption text is rendered + DriverAssert.AssertDriverContentsAre ("Enter text", output); + + // Verify the caption uses dimmed color attribute + Attribute captionAttr = new Attribute ( + tf.GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + tf.GetAttributeForRole (VisualRole.Editable).Background); + + // All characters in "Enter text" should have the caption attribute + DriverAssert.AssertDriverAttributesAre ("0000000000", output, Application.Driver, captionAttr); + + Application.Top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Title_WithHotkey_RendersUnderlined () + { + TextField tf = GetTextFieldsInView (); + + // Title with hotkey should be rendered with the hotkey underlined when not focused + tf.Title = "_Find"; + + // Remove focus so caption appears + Application.RaiseKeyDownEvent ('\t'); + Assert.False (tf.HasFocus); + + View.SetClipToScreen (); + tf.Draw (); + + // The hotkey character 'F' should be rendered (without the underscore in the actual text) + DriverAssert.AssertDriverContentsAre ("Find", output); + + // Verify the hotkey character 'F' has underline style + Attribute captionAttr = new Attribute ( + tf.GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + tf.GetAttributeForRole (VisualRole.Editable).Background); + Attribute hotkeyAttr = new Attribute ( + tf.GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + tf.GetAttributeForRole (VisualRole.Editable).Background, + tf.GetAttributeForRole (VisualRole.Editable).Style | TextStyle.Underline); + + // F is underlined (index 1), remaining characters use normal caption attribute (index 0) + DriverAssert.AssertDriverAttributesAre ("1000", output, Application.Driver, captionAttr, hotkeyAttr); + + Application.Top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Title_WithHotkey_MiddleCharacter_RendersUnderlined () + { + TextField tf = GetTextFieldsInView (); + + // Title with hotkey in middle of text + tf.Title = "Enter _Text"; + + // Remove focus so caption appears + Application.RaiseKeyDownEvent ('\t'); + Assert.False (tf.HasFocus); + + View.SetClipToScreen (); + tf.Draw (); + + // The underscore should not be rendered, 'T' should be underlined + DriverAssert.AssertDriverContentsAre ("Enter Text", output); + + // Verify the hotkey character 'T' has underline style + Attribute captionAttr = new Attribute ( + tf.GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + tf.GetAttributeForRole (VisualRole.Editable).Background); + Attribute hotkeyAttr = new Attribute ( + tf.GetAttributeForRole (VisualRole.Editable).Foreground.GetDimColor (), + tf.GetAttributeForRole (VisualRole.Editable).Background, + tf.GetAttributeForRole (VisualRole.Editable).Style | TextStyle.Underline); + + // "Enter " (6 chars) + "T" (underlined) + "ext" (3 chars) + DriverAssert.AssertDriverAttributesAre ("0000001000", output, Application.Driver, captionAttr, hotkeyAttr); + + Application.Top.Dispose (); + } + [Fact] [TextFieldTestsAutoInitShutdown] public void Changing_SelectedStart_Or_CursorPosition_Update_SelectedLength_And_SelectedText () diff --git a/local_packages/Terminal.Gui.2.0.0.nupkg b/local_packages/Terminal.Gui.2.0.0.nupkg index c8a1980a4..f784c5fef 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.nupkg and b/local_packages/Terminal.Gui.2.0.0.nupkg differ diff --git a/local_packages/Terminal.Gui.2.0.0.snupkg b/local_packages/Terminal.Gui.2.0.0.snupkg index aa76f8d2a..5b2fcc136 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.snupkg and b/local_packages/Terminal.Gui.2.0.0.snupkg differ