diff --git a/Examples/UICatalog/README.md b/Examples/UICatalog/README.md index 07098217c2..042bc8a945 100644 --- a/Examples/UICatalog/README.md +++ b/Examples/UICatalog/README.md @@ -14,10 +14,6 @@ said concepts & features. The original `demo.cs` sample app for Terminal.Gui is neither good to showcase, nor does it explain different concepts. In addition, because it is built on a single source file, it has proven to cause friction when multiple contributors are simultaneously working on different aspects of Terminal.Gui. See [Issue #368](https://github.com/giu-cs/Terminal.Gui/issues/368) for more background. -# API Reference - -* [UI Catalog API Reference](https://gui-cs.github.io/Terminal.Gui/api/UICatalog/UICatalog.html) - ## How To Use Build and run UI Catalog by typing `dotnet run` from the `UI Catalog` folder or by using the `Terminal.Gui` Visual Studio solution. diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index 7365a41390..25d44cf122 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -1,10 +1,7 @@ #nullable enable -using System; using System.Diagnostics; -using System.IO; using System.Reflection; using System.Text; -using System.Threading.Tasks; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -30,7 +27,7 @@ public override void Main () X = 0, Y = 0, Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill () }; _imageView = new ImageView { Width = Dim.Fill (), Height = Dim.Fill ()! - 2 }; @@ -40,14 +37,14 @@ public override void Main () var lbl = new Label { Y = Pos.AnchorEnd (), Text = "Image by Wikiscient" }; win.Add (lbl); - var lbl2 = new Label + Link link = new () { - // This ensures the URL that has an underscore is drawn correctly - HotKeySpecifier = new Rune ('\xFFFF'), - X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), - Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" + X = Pos.AnchorEnd (), + Y = Pos.AnchorEnd (), + Url = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" }; - win.Add (lbl2); + app.ToolTips!.SetToolTip (link, () => link.Url); + win.Add (link); // Start the animation after the window is initialized win.Initialized += OnWinOnInitialized; @@ -55,7 +52,6 @@ public override void Main () app.Run (win); } - private void OnWinOnInitialized (object? sender, EventArgs args) { DirectoryInfo dir; @@ -71,9 +67,7 @@ private void OnWinOnInitialized (object? sender, EventArgs args) dir = new DirectoryInfo (AppContext.BaseDirectory); } - var f = new FileInfo ( - Path.Combine (dir.FullName, "Scenarios/AnimationScenario", "Spinning_globe_dark_small.gif") - ); + var f = new FileInfo (Path.Combine (dir.FullName, "Scenarios/AnimationScenario", "Spinning_globe_dark_small.gif")); if (!f.Exists) { @@ -85,18 +79,16 @@ private void OnWinOnInitialized (object? sender, EventArgs args) _imageView!.SetImage (Image.Load (File.ReadAllBytes (f.FullName))); - Task.Run ( - () => + Task.Run (() => { while (_imageView?.App?.Initialized == true) { // When updating from a Thread/Task always use Invoke - _imageView?.App?.Invoke ( - (_) => - { - _imageView?.NextFrame (); - _imageView?.SetNeedsDraw (); - }); + _imageView?.App?.Invoke (_ => + { + _imageView?.NextFrame (); + _imageView?.SetNeedsDraw (); + }); Task.Delay (100).Wait (); } @@ -171,7 +163,7 @@ private class ImageView : View private Image []? _fullResImages; private Image []? _matchSizes; private Rectangle _oldSize = Rectangle.Empty; - public void NextFrame () { _currentFrame = (_currentFrame + 1) % _frameCount; } + public void NextFrame () => _currentFrame = (_currentFrame + 1) % _frameCount; protected override bool OnDrawingContent (DrawContext? context) { @@ -179,6 +171,7 @@ protected override bool OnDrawingContent (DrawContext? context) { return false; } + if (_oldSize != Viewport) { // Invalidate cached images now size has changed @@ -198,18 +191,14 @@ protected override bool OnDrawingContent (DrawContext? context) int newSize = Math.Min (Viewport.Width, Viewport.Height); // generate one - if (_matchSizes is not null && imgFull is not null) + if (_matchSizes is { } && imgFull is { }) { - _matchSizes [_currentFrame] = imgScaled = imgFull.Clone ( - x => x.Resize ( - newSize * BitmapToBraille.CHAR_HEIGHT, - newSize * BitmapToBraille.CHAR_HEIGHT - ) - ); + _matchSizes [_currentFrame] = + imgScaled = imgFull.Clone (x => x.Resize (newSize * BitmapToBraille.CHAR_HEIGHT, newSize * BitmapToBraille.CHAR_HEIGHT)); } } - if (braille == null && _brailleCache is not null) + if (braille == null && _brailleCache is { }) { _brailleCache [_currentFrame] = braille = GetBraille (_matchSizes? [_currentFrame]!); } @@ -249,11 +238,7 @@ internal void SetImage (Image image) private string GetBraille (Image img) { - var braille = new BitmapToBraille ( - img.Width, - img.Height, - (x, y) => IsLit (img, x, y) - ); + var braille = new BitmapToBraille (img.Width, img.Height, (x, y) => IsLit (img, x, y)); return braille.GenerateImage (); } diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 980eb8b055..bcc4fd9c5d 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -89,8 +89,11 @@ public override void Main () _osIcons.Labels = ["_None", "_Unicode", "Nerd_*"]; win.Add (_osIcons); - win.Add (new Label { Y = Pos.AnchorEnd (2), Text = "* Requires installing Nerd fonts" }); - win.Add (new Label { Y = Pos.AnchorEnd (1), Text = " (see: https://github.com/devblackops/Terminal-Icons)" }); + Label label = new () { Y = Pos.AnchorEnd (), Text = "* Requires installing Nerd fonts:" }; + win.Add (label); + Link link = new () { Y = Pos.Top (label), X = Pos.Right (label) + 1, Url = "https://github.com/devblackops/Terminal-Icons" }; + app.ToolTips!.SetToolTip (link, () => link.Url); + win.Add (link); y = 5; x = 24; diff --git a/Examples/UICatalog/Scenarios/Links.cs b/Examples/UICatalog/Scenarios/Links.cs index 66f682a468..9f7a6cd782 100644 --- a/Examples/UICatalog/Scenarios/Links.cs +++ b/Examples/UICatalog/Scenarios/Links.cs @@ -7,81 +7,91 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Mouse and Keyboard")] public class Links : Scenario { - private IApplication? _app; - private Window? _appWindow; - private Link? _link; - public override void Main () { ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); - _app = app; - _appWindow = new Window { Title = GetName (), BorderStyle = LineStyle.None }; + using Window appWindow = new (); + appWindow.Title = GetName (); + appWindow.BorderStyle = LineStyle.None; Label titleLabel = new () { Text = "_Title:", X = 1, Y = 1 }; - _appWindow.Add (titleLabel); + appWindow.Add (titleLabel); TextField titleTextField = new () { X = Pos.Right (titleLabel) + 1, Y = Pos.Top (titleLabel), Width = Dim.Fill () }; - _appWindow.Add (titleTextField); + appWindow.Add (titleTextField); - Label textLabel = new () { Text = " Te_xt:", X = Pos.Left (titleLabel), Y = Pos.Bottom(titleLabel) }; - _appWindow.Add (textLabel); + Label textLabel = new () { Text = " Te_xt:", X = Pos.Left (titleLabel), Y = Pos.Bottom (titleLabel) }; + appWindow.Add (textLabel); - TextField textTextField = new () { X = Pos.Right (textLabel) + 1, Y = Pos.Top(textLabel), Width = Dim.Fill () }; - _appWindow.Add (textTextField); + TextField textTextField = new () { X = Pos.Right (textLabel) + 1, Y = Pos.Top (textLabel), Width = Dim.Fill () }; + appWindow.Add (textTextField); Label urlLabel = new () { Text = " _Url:", X = 1, Y = Pos.Bottom (titleTextField) + 1 }; - _appWindow.Add (urlLabel); + appWindow.Add (urlLabel); TextField urlTextField = new () { X = Pos.Right (urlLabel) + 1, Y = Pos.Bottom (titleTextField) + 1, Width = Dim.Fill () }; - _appWindow.Add (urlTextField); + appWindow.Add (urlTextField); Label simpleUrlLabel = new () { X = 1, Y = Pos.Bottom (urlTextField) + 2 }; - _appWindow.Add (simpleUrlLabel); + appWindow.Add (simpleUrlLabel); FrameView linkFrame = new () { Title = "_Link Demo", X = 0, Y = Pos.Bottom (simpleUrlLabel) + 2, - Width = Dim.Fill(), + Width = Dim.Fill (), Height = Dim.Auto (), AssignHotKeys = true, - TabStop = TabBehavior.TabStop + TabStop = TabBehavior.TabStop, + Arrangement = ViewArrangement.Resizable }; - _link = new Link { X = 1, Y = 1, BorderStyle = LineStyle.Dotted }; + Link linkWithBorder = new () { BorderStyle = LineStyle.Dotted }; + app.ToolTips!.SetToolTip (linkWithBorder, () => linkWithBorder.Url); - _link.TextChanged += (s, e) => simpleUrlLabel.Text = $"This is just a Label with a URL in Text (WT automatically enables URLs) - {_link.Text}"; - titleTextField.ValueChanged += (s, e) => _link.Title = e.NewValue ?? string.Empty; - textTextField.ValueChanged += (s, e) => _link.Text = e.NewValue ?? string.Empty; - urlTextField.ValueChanged += (s, e) => _link.Url = e.NewValue ?? Link.DEFAULT_URL; - linkFrame.Add (_link); + linkWithBorder.TextChanged += + (_, _) => simpleUrlLabel.Text = $"This is just a Label with a URL in Text (WT automatically enables URLs) - {linkWithBorder.Text}"; + titleTextField.ValueChanged += (_, e) => linkWithBorder.Title = e.NewValue ?? string.Empty; + textTextField.ValueChanged += (_, e) => linkWithBorder.Text = e.NewValue ?? string.Empty; + urlTextField.ValueChanged += (_, e) => linkWithBorder.Url = e.NewValue ?? string.Empty; + linkFrame.Add (linkWithBorder); titleTextField.Text = "Title"; textTextField.Text = "GitHub repo"; urlTextField.Text = "https://github.com/gui-cs/Terminal.Gui"; - Button copyButton = new () { Title = "_Copy", X = Pos.Center (), Y = Pos.AnchorEnd () }; - copyButton.Accepting += (s, e) => _link.Copy (); + Button copyButton = new () { Title = "_Copy", X = Pos.Right (linkWithBorder) + 1, Y = Pos.Top (linkWithBorder) + 1 }; + copyButton.Accepting += (_, _) => linkWithBorder.Copy (); linkFrame.Add (copyButton); - _appWindow.Add (linkFrame); + Label label = new () { Y = Pos.Bottom (linkFrame), Title = "_Link to API Docs:" }; + + Link link = new () + { + X = Pos.Right (label) + 1, Y = Pos.Top (label), Text = "Terminal.Gui.Views.Link", Url = "https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.Views.Link.html" + }; + appWindow.Add (label, link); + app.ToolTips!.SetToolTip (link, () => link.Url); + + appWindow.Add (linkFrame); // StatusBar Shortcut urlIndicator = new (Key.Empty, "", null); - StatusBar statusBar = new ([new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", Quit), urlIndicator]); - _link.MouseEnter += (s, e) => urlIndicator.Title = _link.Text; - _link.MouseLeave += (s, e) => urlIndicator.Title = ""; - _appWindow.Add (statusBar); + StatusBar statusBar = new ([new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", () => appWindow.RequestStop ()), urlIndicator]); - _app.Run (_appWindow); - _appWindow.Dispose (); - } + // Demonstrate dynamically showing URL in the status bar when hovering over the link. + // Note that we use a Shortcut here to show how they can be used in a StatusBar, but you could use any View. + linkWithBorder.MouseEnter += (_, _) => urlIndicator.Title = linkWithBorder.Url; + linkWithBorder.MouseLeave += (_, _) => urlIndicator.Title = ""; - private void Quit () => _appWindow?.RequestStop (); + appWindow.Add (statusBar); + + app.Run (appWindow); + } } diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index d9e51ccaf3..cd9d6c8247 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -1,7 +1,5 @@ #nullable enable using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Text; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Terminal.Gui; @@ -25,12 +23,8 @@ public sealed class UICatalogRunnable : Runnable // the scheme works. public static string? CachedRunnableScheme { get; set; } - // Diagnostics - private static ViewDiagnosticFlags _diagnosticFlags; - public UICatalogRunnable () { - _diagnosticFlags = Diagnostics; SchemeName = CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); ConfigurationManager.Applied += ConfigAppliedHandler; } @@ -62,9 +56,14 @@ public override void BeginInit () /// protected override void OnIsModalChanged (bool newIsModal) { - _disableMouseCb?.Value = App!.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; + if (App is null) + { + return; + } + + _disableMouseCb?.Value = App.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; - _shVersion?.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {App!.Driver!.GetVersionInfo ()}"; + _shVersion?.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {App?.Driver?.GetVersionInfo ()}"; if (string.IsNullOrEmpty ((string?)Result)) { @@ -81,11 +80,11 @@ protected override void OnIsModalChanged (bool newIsModal) if (ShowStatusBar) { - _statusBar!.Height = Dim.Auto (); + _statusBar?.Height = Dim.Auto (); } else { - _statusBar!.Height = 0; + _statusBar?.Height = 0; } } @@ -100,9 +99,11 @@ protected override void OnIsRunningChanged (bool newIsRunning) return; } - if (_scenarioList is { }) + if (_scenarioList is { } && App is { } && _scenarioList.Table is { }) { - ShowScenarioErrorsDialog (App!, (string)_scenarioList.Table! [_scenarioList.SelectedRow, 0], UICatalog.LogCapture.GetScenarioLogs ()); + ShowScenarioErrorsDialog (App, + _scenarioList.Table [_scenarioList.SelectedRow, 0].ToString () ?? string.Empty, + UICatalog.LogCapture.GetScenarioLogs ()); } UICatalog.LogCapture.HasErrors = false; @@ -143,7 +144,7 @@ private MenuBar CreateMenuBar () ]), new MenuBarItem ("_Themes", CreateThemeMenuItems ()), new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems ()), - new MenuBarItem ("_Logging", CreateLoggingMenuItems ()), + new MenuBarItem ("_Logging", CreateLoggingMenuItems ()!), new MenuBarItem (Strings.menuHelp, [ new MenuItem ("_Documentation", @@ -154,14 +155,7 @@ private MenuBar CreateMenuBar () "Project readme", () => Link.OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), Key.F2), - new MenuItem ("_About...", - "About UI Catalog", - () => MessageBox.Query (App!, - "", - GetAboutBoxMessage (), - wrapMessage: false, - buttons: Strings.btnOk), - Key.A.WithCtrl) + new MenuItem ("_About...", "About UI Catalog", ShowAboutDialog, Key.A.WithCtrl) ]) ]) { Title = "menuBar", Id = "menuBar" }; @@ -179,7 +173,7 @@ View [] CreateThemeMenuItems () Action = () => { Driver.Force16Colors = !Driver.Force16Colors; - _force16ColorsShortcutCb!.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; SetNeedsDraw (); } }); @@ -257,10 +251,10 @@ View [] CreateDiagnosticMenuItems () _disableMouseCb = new CheckBox { - Title = "_Disable MouseEventArgs", Value = App!.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked + Title = "_Disable MouseEventArgs", Value = App?.Mouse.IsMouseDisabled == true ? CheckState.Checked : CheckState.UnChecked }; - _disableMouseCb.ValueChanged += (_, args) => { App!.Mouse.IsMouseDisabled = args.NewValue == CheckState.Checked; }; + _disableMouseCb.ValueChanged += (_, args) => { App?.Mouse.IsMouseDisabled = args.NewValue == CheckState.Checked; }; menuItems.Add (new MenuItem { CommandView = _disableMouseCb, HelpText = "Disable MouseEventArgs" }); return menuItems.ToArray (); @@ -421,9 +415,7 @@ View [] CreateLoggingMenuItems () CheckBox drawTraceCheckBox = new () { - Text = "_Draw", - Value = Trace.EnabledCategories.HasFlag (TraceCategory.Draw) ? CheckState.Checked : CheckState.UnChecked, - CanFocus = false + Text = "_Draw", Value = Trace.EnabledCategories.HasFlag (TraceCategory.Draw) ? CheckState.Checked : CheckState.UnChecked, CanFocus = false }; drawTraceCheckBox.ValueChanging += (_, e) => @@ -449,7 +441,13 @@ View [] CreateLoggingMenuItems () void OnLogLevelSelectorOnValueChanged (object? _, ValueChangedEventArgs args) { - UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (Enum.GetValues () [args.NewValue!.Value])! }; + if (args.NewValue is { }) + { + UICatalog.Options = UICatalog.Options with + { + DebugLogLevel = Enum.GetName (Enum.GetValues () [args.NewValue.Value]) ?? string.Empty + }; + } UICatalog.LogLevelSwitch.MinimumLevel = UICatalog.LogLevelToLogEventLevel (Enum.Parse (UICatalog.Options.DebugLogLevel)); } @@ -493,7 +491,11 @@ private void UpdateThemesMenu () CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); } - int newSelectedItem = SchemeManager.GetSchemeNames ().IndexOf (CachedRunnableScheme!); + if (CachedRunnableScheme is null) + { + return; + } + int newSelectedItem = SchemeManager.GetSchemeNames ().IndexOf (CachedRunnableScheme); // if the item is in bounds then select it if (newSelectedItem >= 0 && newSelectedItem < SchemeManager.GetSchemeNames ().Count) @@ -513,18 +515,28 @@ private void UpdateThemesMenu () private TableView CreateScenarioList () { + if (_categoryList is null) + { + throw new InvalidOperationException ("Category list must be created before scenario list"); + } + + if (_menuBar is null) + { + throw new InvalidOperationException ("Menu bar must be created before scenario list"); + } + // Create the scenario list. The contents of the scenario list changes whenever the // Category list selection changes (to show just the scenarios that belong to the selected // category). TableView scenarioList = new () { - X = Pos.Right (_categoryList!) - 1, - Y = Pos.Bottom (_menuBar!), + X = Pos.Right (_categoryList) - 1, + Y = Pos.Bottom (_menuBar), Width = Dim.Fill (), Height = Dim.Height (_categoryList), CanFocus = true, Title = "_Scenarios", - BorderStyle = _categoryList!.BorderStyle, + BorderStyle = _categoryList?.BorderStyle ?? LineStyle.None, SuperViewRendersLineCanvas = true }; @@ -551,7 +563,7 @@ private TableView CreateScenarioList () * we just measure all the data ourselves and set the appropriate * max widths as ColumnStyles */ - int longestName = CachedScenarios!.Max (s => s.GetName ().Length); + int longestName = CachedScenarios?.Max (s => s.GetName ().Length) ?? 0; scenarioList.Style.ColumnStyles.Add (0, new ColumnStyle { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }); scenarioList.Style.ColumnStyles.Add (1, new ColumnStyle { MaxWidth = 1 }); @@ -578,11 +590,15 @@ private TableView CreateScenarioList () private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) { // Save selected item state - _cachedCategoryIndex = _categoryList!.SelectedItem; - _cachedScenarioIndex = _scenarioList!.SelectedRow; + _cachedCategoryIndex = _categoryList?.SelectedItem; - // Set the Result to the selected scenario name - Result = (string)_scenarioList.Table! [_scenarioList.SelectedRow, 0]; + if (_scenarioList is { }) + { + _cachedScenarioIndex = _scenarioList.SelectedRow; + + // Set the Result to the selected scenario name + Result = _scenarioList.Table? [_scenarioList.SelectedRow, 0]; + } Logging.Information ($"Scenario Selected; Stopping {GetType ().Name}: {Result}"); App?.RequestStop (); } @@ -597,13 +613,23 @@ private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) private ListView CreateCategoryList () { + if (_menuBar is null) + { + throw new InvalidOperationException ("Menu bar must be created before scenario list"); + } + + if (_statusBar is null) + { + throw new InvalidOperationException ("Status bar must be created before scenario list"); + } + // Create the Category list view. This list never changes. ListView categoryList = new () { X = 0, - Y = Pos.Bottom (_menuBar!), + Y = Pos.Bottom (_menuBar), Width = Dim.Auto (), - Height = Dim.Fill (_statusBar!), + Height = Dim.Fill (_statusBar), ShowMarks = false, CanFocus = true, Title = "_Categories", @@ -614,7 +640,7 @@ private ListView CreateCategoryList () categoryList.Accepting += (_, e) => { - _scenarioList!.SetFocus (); + _scenarioList?.SetFocus (); e.Handled = true; }; categoryList.ValueChanged += CategoryView_SelectedChanged; @@ -627,24 +653,25 @@ private ListView CreateCategoryList () private void CategoryView_SelectedChanged (object? sender, ValueChangedEventArgs e) { - if (e.NewValue is null) + if (e.NewValue is null || CachedCategories is null) { return; } - string item = CachedCategories! [e.NewValue.Value]; - ObservableCollection newScenarioList; + string item = CachedCategories [e.NewValue.Value]; - if (e.NewValue == 0) - { - // First category is "All" - newScenarioList = CachedScenarios!; - } - else + if (CachedScenarios is null) { - newScenarioList = new ObservableCollection (CachedScenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ()); + return; } - _scenarioList!.Table = new EnumerableTableSource (newScenarioList, + // First category is "All" + ObservableCollection newScenarioList = e.NewValue == 0 + ? CachedScenarios + : new ObservableCollection (CachedScenarios + .Where (s => s.GetCategories ().Contains (item)) + .ToList ()); + + _scenarioList?.Table = new EnumerableTableSource (newScenarioList, new Dictionary> { { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () } @@ -661,7 +688,7 @@ private void CategoryView_SelectedChanged (object? sender, ValueChangedEventArgs [JsonPropertyName ("UICatalog.StatusBar")] public static bool ShowStatusBar { - get => field; + get; set { if (field == value) @@ -706,7 +733,7 @@ private StatusBar CreateStatusBar () Action = () => { Driver.Force16Colors = !Driver.Force16Colors; - _force16ColorsMenuItemCb!.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsMenuItemCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; SetNeedsDraw (); } }; @@ -722,12 +749,12 @@ private StatusBar CreateStatusBar () switch (args.NewValue) { case true: - _statusBar!.Height = Dim.Auto (); + _statusBar?.Height = Dim.Auto (); break; case false: - _statusBar!.Height = 0; + _statusBar?.Height = 0; break; } @@ -751,10 +778,10 @@ private void ConfigApplied () _shQuit?.Key = Application.GetDefaultKey (Command.Quit); - _disableMouseCb!.Value = App!.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; - _force16ColorsShortcutCb!.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _disableMouseCb?.Value = App?.Mouse.IsMouseDisabled == true ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb?.Value = Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - App.TopRunnableView?.SetNeedsDraw (); + App?.TopRunnableView?.SetNeedsDraw (); } private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) => ConfigApplied (); @@ -762,31 +789,121 @@ private void ConfigApplied () #endregion Configuration Manager /// - /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests. + /// The URL displayed in the About Box. /// - /// - public static string GetAboutBoxMessage () + public const string ABOUT_URL = "https://github.com/gui-cs/Terminal.Gui"; + + private void ShowAboutDialog () { - // NOTE: Do not use multiline verbatim strings here. - // WSL gets all confused. - StringBuilder msg = new (); - msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for"); - msg.AppendLine (); - - msg.AppendLine (""" - _______ _ _ _____ _ - |__ __| (_) | | / ____| (_) - | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ - | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | - | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | - |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| - """); - msg.AppendLine (); - msg.AppendLine ("v2 - Beta"); - msg.AppendLine (); - msg.Append ("https://github.com/gui-cs/Terminal.Gui"); - - return msg.ToString (); + Dialog dialog = new () { Title = "", Buttons = [new Button { Title = Strings.btnOk, IsDefault = true }] }; + + Label tagline = new () + { + Text = "UI Catalog: A comprehensive sample library and test app for", + TextAlignment = Alignment.Center, + X = Pos.Center (), + Width = Dim.Auto (DimAutoStyle.Text), + Height = Dim.Auto (DimAutoStyle.Text) + }; + + GradientArtView asciiArt = new () { X = Pos.Center (), Y = Pos.Bottom (tagline) + 1 }; + + Link link = new () { Text = ABOUT_URL, Url = ABOUT_URL, X = Pos.Center (), Y = Pos.Bottom (asciiArt) + 1 }; + App?.ToolTips?.SetToolTip (link, () => link.Url); + + dialog.Add (tagline, asciiArt, link); + dialog.Buttons.ElementAt (0).SetFocus (); + App?.Run (dialog); + dialog.Dispose (); + } + + /// + /// Renders the Terminal.Gui logo in box-drawing characters with a diagonal gradient. + /// + private sealed class GradientArtView : View + { + // @formatter:off + private const string ART = """ + ╺┳╸┏━╸┏━┓┏┳┓╻┏┓╻┏━┓╻ ┏━╸╻ ╻╻ + ┃ ┣╸ ┣┳┛┃┃┃┃┃┗┫┣━┫┃ ┃╺┓┃ ┃┃ + ╹ ┗━╸╹┗╸╹ ╹╹╹ ╹╹ ╹┗━╸╹┗━┛┗━┛╹ + + v2 - Beta + """; + + // @formatter:on + + private static readonly string [] _artLines = ART.ReplaceLineEndings ("\n").Split ('\n'); + + public GradientArtView () + { + int artWidth = _artLines.Select (line => line.Length).Prepend (0).Max (); + + Width = artWidth; + Height = _artLines.Length; + } + + /// + protected override bool OnDrawingContent (DrawContext? context) + { + List stops = + [ + new (0, 128, 255), // Bright Blue + new (0, 255, 128), // Bright Green + new (255, 255), // Bright Yellow + new (255, 128) // Bright Orange + ]; + + List steps = [10]; + + Gradient gradient = new (stops, steps); + + var artHeight = 3; // Only the box-drawing lines get the gradient + int artWidth = _artLines [0].Length; + + Dictionary colorMap = gradient.BuildCoordinateColorMapping (artHeight, artWidth, GradientDirection.Diagonal); + + Attribute normalAttr = GetAttributeForRole (VisualRole.Normal); + + for (var row = 0; row < _artLines.Length; row++) + { + string line = _artLines [row]; + + for (var col = 0; col < line.Length; col++) + { + char ch = line [col]; + + if (ch == ' ') + { + continue; + } + + // Gradient only on the 3 art lines; version text uses normal color + if (row < 3) + { + Point coord = new (col, row); + + if (colorMap.TryGetValue (coord, out Color color)) + { + SetAttribute (new Attribute (color, normalAttr.Background)); + } + else + { + SetAttribute (normalAttr); + } + } + else + { + SetAttribute (normalAttr); + } + + Move (col, row); + AddStr (ch.ToString ()); + } + } + + return true; + } } /// diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 9bd1b6c308..8a0178196c 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -235,7 +235,12 @@ public void ResetState (bool ignoreDisposed = false) Popovers?.Dispose (); Popovers = null; - // === 3. Clean up runnables === + // === 3. Close and dispose tooltips === + // Any tooltips added to ToolTips have their lifetime controlled by ToolTips + ToolTips?.Dispose (); + ToolTips = null; + + // === 4. Clean up runnables === SessionStack?.Clear (); #if DEBUG_IDISPOSABLE diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 9a3700b921..a5d8c3bc18 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -44,8 +44,7 @@ internal ApplicationImpl () : this (new SystemTimeProvider ()) { } /// /// The component factory. /// Time provider for timestamps and timing control. - internal ApplicationImpl (IComponentFactory componentFactory, ITimeProvider timeProvider) : this (timeProvider) => - _componentFactory = componentFactory; + internal ApplicationImpl (IComponentFactory componentFactory, ITimeProvider timeProvider) : this (timeProvider) => _componentFactory = componentFactory; private string? _driverName; @@ -57,7 +56,7 @@ internal ApplicationImpl (IComponentFactory componentFactory, ITimeProvider time /// /// Lock object for synchronizing access to ModelUsage and _instance. /// - private static readonly object _modelUsageLock = new (); + private static readonly Lock _modelUsageLock = new (); /// /// Tracks which application model has been used in this process. @@ -174,11 +173,7 @@ internal static void ResetStateStatic (bool ignoreDisposed = false) #region Screen and Driver /// - public IClipboard? Clipboard - { - get => Driver?.Clipboard; - set => Driver?.Clipboard = value; - } + public IClipboard? Clipboard { get => Driver?.Clipboard; set => Driver?.Clipboard = value; } #endregion Screen and Driver @@ -259,5 +254,16 @@ public ApplicationPopover? Popovers set; } + public ApplicationToolTip? ToolTips + { + get + { + field ??= new ApplicationToolTip { App = this }; + + return field; + } + set; + } + #endregion Navigation and Popover } diff --git a/Terminal.Gui/App/ApplicationToolTip.cs b/Terminal.Gui/App/ApplicationToolTip.cs new file mode 100644 index 0000000000..6700100aea --- /dev/null +++ b/Terminal.Gui/App/ApplicationToolTip.cs @@ -0,0 +1,215 @@ +using System.ComponentModel; + +namespace Terminal.Gui.App; + +/// +/// Manages tooltip behavior for a given . +/// +/// +/// +/// This manager provides a shared tooltip instance and associates tooltip content with multiple views. +/// It centralizes hover handling (MouseEnter / MouseLeave) and ensures that only one tooltip is visible at a time. +/// +/// +/// ToolTip content is defined per using a factory (), +/// allowing dynamic content creation on each display. +/// +/// +/// The manager avoids adding state directly to by maintaining an external registry. +/// +/// +public sealed class ApplicationToolTip : IDisposable +{ + /// + /// The instance used by this instance. + /// + public IApplication? App { get; set; } + + // Stores tooltip registrations for each target view + internal readonly Dictionary _registrations = new (); + + // Shared tooltip instance reused across all views + private ToolTipHost? _sharedToolTip; + + // Currently active target view (if any) + private View? _currentTarget; + + /// + /// Sets the tooltip text for the specified view. + /// + /// The view to associate with the tooltip. Cannot be null. + /// The text to display in the tooltip. Cannot be null. + public void SetToolTip (View target, string text) + { + ArgumentNullException.ThrowIfNull (target); + ArgumentNullException.ThrowIfNull (text); + + SetToolTip (target, new ToolTipProvider (text)); + } + + /// + /// Associates a tooltip with the specified view, using a delegate to dynamically provide the tooltip text. + /// + /// + /// Use this method when the tooltip text may change over time or depends on runtime conditions. + /// The tooltip text is evaluated on demand by calling the provided delegate. + /// + /// The view to which the tooltip will be attached. Cannot be null. + /// + /// A delegate that returns the tooltip text to display. Cannot be null. The delegate is invoked each time the + /// tooltip is shown. + /// + public void SetToolTip (View target, Func textFactory) + { + ArgumentNullException.ThrowIfNull (target); + ArgumentNullException.ThrowIfNull (textFactory); + + SetToolTip (target, new ToolTipProvider (textFactory)); + } + + /// + /// Associates a tooltip with the specified target view using a factory function to generate the tooltip content. + /// + /// + /// The tooltip content is created on demand by invoking the provided factory function each time + /// the tooltip is displayed. This allows for dynamic or context-sensitive tooltip content. + /// + /// The view to which the tooltip will be attached. Cannot be null. + /// A function that returns the view to be used as the tooltip content. Cannot be null. + public void SetToolTip (View target, Func contentFactory) + { + ArgumentNullException.ThrowIfNull (target); + ArgumentNullException.ThrowIfNull (contentFactory); + + SetToolTip (target, new ToolTipProvider (contentFactory)); + } + + /// + /// Registers a tooltip provider for the specified view, enabling tooltips to be displayed when the user hovers over + /// the view. + /// + /// + /// If a tooltip provider is already registered for the specified view, it will be replaced by + /// the new provider. ToolTips are shown when the mouse enters the view and hidden when the mouse leaves. To remove + /// a tooltip, use the appropriate removal method. + /// + /// The view for which the tooltip should be displayed. Cannot be null. + /// The provider that supplies tooltip content for the target view. Cannot be null. + internal void SetToolTip (View target, ToolTipProvider provider) + { + ArgumentNullException.ThrowIfNull (target); + ArgumentNullException.ThrowIfNull (provider); + + RemoveToolTip (target); + + target.MouseEnter += OnMouseEnter; + target.MouseLeave += OnMouseLeave; + target.Disposing += OnDisposing; + + _registrations [target] = new ToolTipRegistration (OnMouseEnter, OnMouseLeave, OnDisposing); + + return; + + void OnMouseLeave (object? sender, EventArgs e) + { + if (_currentTarget == target) + { + Hide (); + } + } + + void OnMouseEnter (object? sender, CancelEventArgs e) => ShowFor (target, provider); + } + + private void OnDisposing (object? sender, EventArgs e) + { + if (sender is View target) + { + target.App?.ToolTips?.RemoveToolTip (target); + } + } + + /// + /// Removes the tooltip associated with a target view. + /// + /// The target view. + /// + /// This unsubscribes from events and removes any stored tooltip content. + /// + public void RemoveToolTip (View target) + { + ArgumentNullException.ThrowIfNull (target); + + if (_registrations.TryGetValue (target, out ToolTipRegistration? registration)) + { + target.MouseEnter -= registration.MouseEnter; + target.MouseLeave -= registration.MouseLeave; + target.Disposing -= registration.Disposing; + + _registrations.Remove (target); + } + + if (_currentTarget == target) + { + Hide (); + } + } + + /// + /// Displays a tooltip for the specified target view using the provided tooltip content provider. + /// + /// + /// If a tooltip is already visible for another view, it will be updated to display content for + /// the new target. The tooltip is anchored relative to the target view's position on the screen. + /// + /// The view for which the tooltip will be shown. Cannot be null. + /// The provider that supplies the tooltip content for the target view. Cannot be null. + public void ShowFor (View target, ToolTipProvider provider) + { + ArgumentNullException.ThrowIfNull (target); + ArgumentNullException.ThrowIfNull (provider); + + _sharedToolTip ??= new ToolTipHost (); + _sharedToolTip.App ??= target.App; + _sharedToolTip.Anchor = () => target.FrameToScreen (); + _sharedToolTip.SetContent (provider.GetContent); + + _currentTarget = target; + _sharedToolTip.MakeVisible (); + } + + /// + /// Hides the currently visible tooltip. + /// + public void Hide () + { + _sharedToolTip?.Visible = false; + _currentTarget = null; + } + + /// + /// Releases all resources used by the manager. + /// + /// + /// This unsubscribes all event handlers and disposes the shared tooltip. + /// + public void Dispose () + { + foreach ((View target, ToolTipRegistration registration) in _registrations) + { + target.MouseEnter -= registration.MouseEnter; + target.MouseLeave -= registration.MouseLeave; + target.Disposing -= registration.Disposing; + } + + _registrations.Clear (); + _sharedToolTip?.Dispose (); + _sharedToolTip = null; + _currentTarget = null; + } + + /// + /// Stores event handlers and content factory for a registered view. + /// + internal sealed record ToolTipRegistration (EventHandler MouseEnter, EventHandler MouseLeave, EventHandler Disposing); +} diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 07fff4f6f4..333639eb10 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -267,8 +267,7 @@ public interface IApplication : IDisposable /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public IApplication Run (Func? errorHandler = null, string? driverName = null) - where TRunnable : IRunnable, new(); + public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new (); #region Iteration & Invoke @@ -424,7 +423,7 @@ public IApplication Run (Func? errorHandler = null, /// } /// /// - T? GetResult () where T : class { return GetResult () as T; } + T? GetResult () where T : class => GetResult () as T; #endregion Result Management @@ -583,6 +582,16 @@ public IApplication Run (Func? errorHandler = null, /// ApplicationPopover? Popovers { get; set; } + /// + /// Gets or sets the tool tip manager used to display contextual help for UI elements within the application. + /// + /// + /// Assigning a value to this property enables tool tip functionality, allowing users to receive + /// additional information when interacting with supported controls. If set to null, tool tips are disabled for the + /// application. + /// + ApplicationToolTip? ToolTips { get; set; } + #endregion Navigation and Popover #region Timeouts diff --git a/Terminal.Gui/App/Popovers/ToolTipHost.cs b/Terminal.Gui/App/Popovers/ToolTipHost.cs new file mode 100644 index 0000000000..450b811def --- /dev/null +++ b/Terminal.Gui/App/Popovers/ToolTipHost.cs @@ -0,0 +1,255 @@ +namespace Terminal.Gui.App; + +/// +/// A generic tooltip view that hosts a view and can be shown by a ToolTipManager. +/// +/// +/// The type of view being hosted. Must derive from and have a parameterless constructor. +/// +/// +/// +/// This class is inspired by but simplified for tooltip scenarios +/// where no result extraction is needed. +/// +/// +/// This class is responsible only for hosting, positioning and displaying tooltip content. +/// Hover handling and target registration should be managed externally by a tooltip manager. +/// +/// +public class ToolTipHost : PopoverImpl, IDesignable where TView : View, new() +{ + private CommandBridge? _contentCommandBridge; + + /// + /// Initializes a new instance of the class. + /// + public ToolTipHost () : this (null) { } + + /// + /// Initializes a new instance of the class with the specified content view. + /// + /// + /// The view to host in the tooltip. If , a new instance will be created. + /// + public ToolTipHost (TView? contentView) + { + // Do this to support debugging traces where Title gets set + // Unicode Character 'REPLACEMENT CHARACTER' (U+FFFF) is used to indicate an invalid HotKeySpecifier + base.HotKeySpecifier = (Rune)'\xffff'; + + Border.Settings &= ~BorderSettings.Title; + + base.Visible = false; + +#if DEBUG + if (string.IsNullOrEmpty (contentView?.Id)) + { + contentView?.Id = $"tooltipContentView_{Id}"; + } +#endif + + ContentView = contentView; + } + + /// + /// Gets or sets the content view hosted by this tooltip. + /// + public TView? ContentView + { + get; + set + { + if (field is null && value is null) + { + value = new TView (); + } + else if (ReferenceEquals (field, value)) + { + return; + } + +#if DEBUG + Id = $"{value?.Id}ToolTip"; +#endif + + if (field is { }) + { + field.VisibleChanged -= ContentViewOnVisibleChanged; + Remove (field); + field.Dispose (); + } + + field = value ?? new TView (); + + field.App = App; + Add (field); + + field.VisibleChanged += ContentViewOnVisibleChanged; + + _contentCommandBridge?.Dispose (); + _contentCommandBridge = CommandBridge.Connect (this, field, Command.Activate, Command.Accept); + } + } + + /// + /// Makes the tooltip visible and positions it based on or + /// . + /// + /// + /// The ideal screen-relative position for the tooltip. If , positioning is determined + /// by or the current mouse position. + /// + /// + /// Optional anchor rectangle to override property for this call. + /// If , uses the property. + /// + public override void MakeVisible (Point? idealScreenPosition = null, Rectangle? anchor = null) + { + Layout (); + SetPosition (idealScreenPosition, anchor); + + if (App?.Popovers is not { } popovers) + { + return; + } + + if (!popovers.IsRegistered (this)) + { + popovers.Register (this); + } + + popovers.Show (this); + } + + /// + /// Sets the position of the tooltip based on or . + /// + public void SetPosition (Point? idealScreenPosition = null, Rectangle? anchor = null) + { + Rectangle? effectiveAnchor = anchor ?? Anchor?.Invoke (); + + if (effectiveAnchor is { }) + { + idealScreenPosition = new Point (effectiveAnchor.Value.X, effectiveAnchor.Value.Y + effectiveAnchor.Value.Height); + } + + idealScreenPosition ??= App?.Mouse.LastMousePosition; + + if (idealScreenPosition is null || ContentView is null) + { + return; + } + + Point pos = idealScreenPosition.Value; + + if (!ContentView.IsInitialized) + { + ContentView.App ??= App; + ContentView.BeginInit (); + ContentView.EndInit (); + } + + pos = GetAdjustedPosition (ContentView, pos); + + ContentView.X = pos.X; + ContentView.Y = pos.Y; + } + + /// + /// Calculates an adjusted screen-relative position for the content view to ensure full visibility. + /// + protected virtual Point GetAdjustedPosition (View view, Point idealLocation) + { + int screenWidth = App?.Screen.Width ?? 0; + int screenHeight = App?.Screen.Height ?? 0; + + int viewWidth = view.Frame.Width; + int viewHeight = view.Frame.Height; + + int nx = idealLocation.X; + + if (nx + viewWidth > screenWidth) + { + nx = Math.Max (screenWidth - viewWidth, 0); + } + + nx = Math.Max (nx, 0); + + int ny = idealLocation.Y; + + if (ny + viewHeight > screenHeight) + { + ny = idealLocation.Y - viewHeight - 1; + } + + ny = Math.Max (ny, 0); + + return new Point (nx, ny); + } + + /// + protected override void OnVisibleChanged () + { + if (Visible) + { + ContentView?.Visible = true; + } + else + { + ContentView?.Visible = false; + } + + base.OnVisibleChanged (); + } + + /// + /// Called when the ContentView becomes invisible, to hide the ToolTip. + /// + private void ContentViewOnVisibleChanged (object? sender, EventArgs e) + { + if (sender is View { Visible: false } view && view == ContentView && Visible) + { + Visible = false; + } + } + + /// + /// Replaces the current content with a newly created view. + /// + public void SetContent (Func contentFactory) + { + ArgumentNullException.ThrowIfNull (contentFactory); + ContentView = contentFactory (); + } + + /// + /// Enables the tooltip for use in design-time scenarios. + /// + public virtual bool EnableForDesign (ref TContext targetView) where TContext : notnull + { + ContentView ??= new TView (); + + if (ContentView is IDesignable designable) + { + return designable.EnableForDesign (ref targetView); + } + + return true; + } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + ContentView?.VisibleChanged -= ContentViewOnVisibleChanged; + ContentView?.Dispose (); + ContentView = null; + + _contentCommandBridge?.Dispose (); + _contentCommandBridge = null; + } + + base.Dispose (disposing); + } +} \ No newline at end of file diff --git a/Terminal.Gui/App/Popovers/ToolTipProvider.cs b/Terminal.Gui/App/Popovers/ToolTipProvider.cs new file mode 100644 index 0000000000..966bd94ea0 --- /dev/null +++ b/Terminal.Gui/App/Popovers/ToolTipProvider.cs @@ -0,0 +1,52 @@ +namespace Terminal.Gui.App; + +/// +/// Provides a mechanism for supplying tooltip content for ToolTipHost. +/// +/// ToolTipProvider enables flexible creation of tooltips by allowing content to be specified as a static +/// string, a dynamic string factory, a custom view, or a view factory delegate. This allows tooltips to be customized +/// or generated dynamically based on application state. The tooltip content is created on demand when needed by the +/// UI. +public sealed class ToolTipProvider +{ + private readonly Func _factory; + + /// + /// Creates tooltip content. + /// + public View GetContent () => _factory (); + + /// + /// Initializes a new instance of the ToolTipProvider class using the specified factory function to generate tooltip + /// content. + /// + /// A function that returns a View representing the content to display in the tooltip. This function is invoked each + /// time a tooltip is shown. + /// Thrown if the contentFactory parameter is null. + public ToolTipProvider (Func contentFactory) + { + _factory = contentFactory ?? throw new ArgumentNullException (nameof (contentFactory)); + } + + /// + /// Initializes a new instance of the ToolTipProvider class with the specified tooltip text. + /// + /// The text to display in the tooltip. This value is used as the content of the tooltip label. + public ToolTipProvider (string text) + : this (() => new Label { Text = text }) + { + } + + /// + /// Initializes a new instance of the ToolTipProvider class using a delegate that supplies the tooltip text. + /// + /// Use this constructor when you want the tooltip text to be generated dynamically at runtime. + /// The provided delegate allows the tooltip content to reflect the current state or context when + /// displayed. + /// A delegate that returns the text to display in the tooltip. The delegate is invoked each time the tooltip is + /// shown. + public ToolTipProvider (Func textFactory) + : this (() => new Label { Text = textFactory () }) + { + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Link.cs b/Terminal.Gui/Views/Link.cs index 2bd7ccfc51..219b141edb 100644 --- a/Terminal.Gui/Views/Link.cs +++ b/Terminal.Gui/Views/Link.cs @@ -57,7 +57,8 @@ namespace Terminal.Gui.Views; /// Mouse Event Action /// /// -/// Click Accepts the link, opening the URL (). +/// Click +/// Activate the link, opening the URL (). /// /// /// @@ -73,8 +74,6 @@ public Link () // On HotKey, pass it to the next view AddCommand (Command.HotKey, InvokeHotKeyOnNextPeer!); - - MouseBindings.Add (MouseFlags.LeftButtonClicked, Command.Accept); } /// @@ -97,11 +96,11 @@ protected override bool OnActivating (CommandEventArgs args) /// Called when the link is accepted (e.g., clicked or is invoked). /// Opens in the default browser via . /// - protected override void OnAccepted (ICommandContext? ctx) + protected override void OnActivated (ICommandContext? ctx) { - base.OnAccepted (ctx); - OpenUrl (Url); + + base.OnActivated (ctx); } /// @@ -118,7 +117,7 @@ protected override void OnAccepted (ICommandContext? ctx) /// The URL to open. Should be a well-formed absolute URI. public static void OpenUrl (string url) { - if (!Uri.IsWellFormedUriString (url, UriKind.Absolute)) + if (!Uri.IsWellFormedUriString (url, UriKind.Absolute) || Environment.GetEnvironmentVariable ("DisableRealDriverIO") == "1") { return; } @@ -174,13 +173,8 @@ protected override void UpdateTextFormatterText () } } - /// - /// The default value for — an empty string indicating no URL is associated with the link. - /// - public const string DEFAULT_URL = ""; - - private string _url = DEFAULT_URL; - private bool _isUrlValid = false; + private string _url = ""; + private bool _isUrlValid; /// /// Gets or sets the URL (hyperlink target) associated with this . @@ -247,27 +241,20 @@ protected virtual void OnUrlChanged (ValueChangedEventArgs args) { } return true; } - if (HotKey.IsValid) + if (!HotKey.IsValid) { - // If the Link has a hotkey, we need to find the next view in the subview list - int me = SuperView?.SubViews.IndexOf (this) ?? -1; - - if (me != -1 && me < SuperView?.SubViews.Count - 1) - { - return SuperView?.SubViews.ElementAt (me + 1).InvokeCommand (Command.HotKey) == true; - } + return false; } - return false; - } + // If the Link has a hotkey, we need to find the next view in the subview list + int me = SuperView?.SubViews.IndexOf (this) ?? -1; - /// - bool IDesignable.EnableForDesign () - { - Title = "_Link"; - Url = "https://github.com/gui-cs"; + if (me != -1 && me < SuperView?.SubViews.Count - 1) + { + return SuperView?.SubViews.ElementAt (me + 1).InvokeCommand (Command.HotKey) == true; + } - return true; + return false; } /// @@ -384,4 +371,17 @@ private void SetUrl (string value) TextFormatter.NeedsFormat = true; SetNeedsLayout (); } + + /// + bool IDesignable.EnableForDesign () + { + Title = "_Link"; + Url = "https://github.com/gui-cs"; + + Initialized += (_, _) => { App?.ToolTips?.SetToolTip (this, "This is a Link. Click to open the URL in the default browser."); }; + + return true; + } + + } diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 0159621e5b..4e9a80da34 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -547,6 +547,7 @@ True True True + True True True True diff --git a/Tests/UnitTestsParallelizable/Application/Popover/ApplicationToolTipTests.cs b/Tests/UnitTestsParallelizable/Application/Popover/ApplicationToolTipTests.cs new file mode 100644 index 0000000000..01ef328a48 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Popover/ApplicationToolTipTests.cs @@ -0,0 +1,83 @@ +using System.ComponentModel; +using JetBrains.Annotations; + +namespace ApplicationTests.Popover; + +/// +/// Contains unit tests for the ToolTipManager class. +/// +[TestSubject (typeof (ApplicationToolTip))] +public class ApplicationToolTipTests +{ + [Fact] + public void SetToolTip_NullTarget_ThrowsArgumentNullException () + { + ApplicationToolTip manager = new (); + ToolTipProvider provider = new (() => new Label { Text = "Test" }); + + Assert.Throws (() => manager.SetToolTip (null!, provider)); + } + + [Fact] + public void SetToolTip_NullProvider_ThrowsArgumentNullException () + { + ApplicationToolTip manager = new (); + View view = new (); + + Assert.Throws (() => manager.SetToolTip (view, (string?)null!)); + } + + [Fact] + public void SetToolTip_ValidParameters_SetsRemovesToolTip () + { + ApplicationToolTip manager = new (); + View view = new (); + ToolTipProvider provider = new (() => new Label { Text = "Test" }); + + manager.SetToolTip (view, provider); + + Assert.True (manager._registrations.ContainsKey (view)); + + manager.RemoveToolTip (view); + + Assert.False (manager._registrations.ContainsKey (view)); + } + + [Fact] + public void EnterView_ShowsHidesToolTip () + { + using IApplication app = Application.Create ().Init (); + + using Runnable window = new (); + window.Width = Dim.Fill (); + window.Height = Dim.Fill (); + window.BorderStyle = LineStyle.None; + + View view = new (); + window.Add (view); + View toolTipContent = new (); + + app.ToolTips!.SetToolTip (view, () => toolTipContent); + + app.Begin (window); + app.LayoutAndDraw (); + app.Driver!.Refresh (); + + // Simulate mouse enter event + CancelEventArgs eventArgs = new (); + _ = view.NewMouseEnterEvent (eventArgs); + + Assert.True (toolTipContent.Visible); + + // The tooltip host of the view should be visible + Assert.True (toolTipContent.SuperView!.Visible); + + // Simulate mouse leave event + view.NewMouseLeaveEvent (); + + Assert.False (toolTipContent.Visible); + + // The tooltip host of the view should be hidden + Assert.False (toolTipContent.SuperView!.Visible); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/Popover/ToolTipHostTests.cs b/Tests/UnitTestsParallelizable/Application/Popover/ToolTipHostTests.cs new file mode 100644 index 0000000000..9225beb785 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/Popover/ToolTipHostTests.cs @@ -0,0 +1,292 @@ +using JetBrains.Annotations; + +namespace ApplicationTests.Popover; + +/// +/// Tests for . +/// +[TestSubject (typeof (ToolTipHost<>))] +public class ToolTipHostTests +{ + [Fact] + public void Constructor_SetsDefaults () + { + ToolTipHost