diff --git a/Examples/PopoverWrapperExample/PopoverWrapperExample.csproj b/Examples/PopoverWrapperExample/PopoverWrapperExample.csproj new file mode 100644 index 000000000..7e34acedb --- /dev/null +++ b/Examples/PopoverWrapperExample/PopoverWrapperExample.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + latest + + + + + + + diff --git a/Examples/PopoverWrapperExample/Program.cs b/Examples/PopoverWrapperExample/Program.cs new file mode 100644 index 000000000..86a1fd873 --- /dev/null +++ b/Examples/PopoverWrapperExample/Program.cs @@ -0,0 +1,417 @@ +// Example demonstrating how to make ANY View into a popover without implementing IPopover + +using Terminal.Gui; +using Terminal.Gui.App; +using Terminal.Gui.Configuration; +using Terminal.Gui.Drawing; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; +using Attribute = Terminal.Gui.Drawing.Attribute; + +IApplication app = Application.Create (); +app.Init (); + +// Create a main window with some buttons to trigger popovers +Window mainWindow = new () +{ + Title = "PopoverWrapper Example - Press buttons to show popovers", + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () +}; + +Label label = new () +{ + Text = "Click buttons below or press their hotkeys to show different popovers.\nPress Esc to close a popover.", + X = Pos.Center (), + Y = 1, + Width = Dim.Fill (), + Height = 2, + TextAlignment = Alignment.Center +}; + +mainWindow.Add (label); + +// Example 1: Simple view as popover +Button button1 = new () +{ + Title = "_1: Simple View Popover", + X = Pos.Center (), + Y = Pos.Top (label) + 3 +}; + +button1.Accepting += (s, e) => +{ + IApplication? application = (s as View)?.App; + + if (application is null) + { + return; + } + + View simpleView = new () + { + Title = "Simple Popover", + Width = Dim.Auto (), + Height = Dim.Auto (), + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu) + }; + + simpleView.Add ( + new Label + { + Text = "This is a simple View wrapped as a popover!\n\nPress Esc or click outside to dismiss.", + X = Pos.Center (), + Y = Pos.Center (), + TextAlignment = Alignment.Center + }); + + PopoverWrapper popover = simpleView.AsPopover (); + popover.X = Pos.Center (); + popover.Y = Pos.Center (); + application.Popover?.Register (popover); + application.Popover?.Show (popover); + + e.Handled = true; +}; + +mainWindow.Add (button1); + +// Example 2: ListView as popover +Button button2 = new () +{ + Title = "_2: ListView Popover", + X = Pos.Center (), + Y = Pos.Bottom (button1) + 1 +}; + +ListView listView = new () +{ + Title = "Select an Item", + X = Pos.Center (), + Y = Pos.Center (), + Width = Dim.Percent (30), + Height = Dim.Percent (40), + BorderStyle = LineStyle.Single, + Source = new ListWrapper (["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"]), + SelectedItem = 0 +}; + +PopoverWrapper listViewPopover = listView.AsPopover (); + +listView.SelectedItemChanged += (sender, args) => +{ + listViewPopover.Visible = false; + + if (listView.SelectedItem is { } selectedItem && selectedItem >= 0) + { + MessageBox.Query (app, "Selected", $"You selected: {listView.Source.ToList () [selectedItem]}", "OK"); + } +}; + +button2.Accepting += (s, e) => +{ + IApplication? application = (s as View)?.App; + + if (application is null) + { + return; + } + + listViewPopover.X = Pos.Center (); + listViewPopover.Y = Pos.Center (); + application.Popover?.Register (listViewPopover); + application.Popover?.Show (listViewPopover); + + e.Handled = true; +}; + +mainWindow.Add (button2); + +// Example 3: Form as popover +Button button3 = new () +{ + Title = "_3: Form Popover", + X = Pos.Center (), + Y = Pos.Bottom (button2) + 1 +}; + +button3.Accepting += (s, e) => +{ + IApplication? application = (s as View)?.App; + + if (application is null) + { + return; + } + + View formView = CreateFormView (application); + PopoverWrapper popover = formView.AsPopover (); + popover.X = Pos.Center (); + popover.Y = Pos.Center (); + application.Popover?.Register (popover); + application.Popover?.Show (popover); + e.Handled = true; +}; + +mainWindow.Add (button3); + +// Example 4: ColorPicker as popover +Button button4 = new () +{ + Title = "_4: ColorPicker Popover", + X = Pos.Center (), + Y = Pos.Bottom (button3) + 1 +}; + +button4.Accepting += (s, e) => +{ + IApplication? application = (s as View)?.App; + + if (application is null) + { + return; + } + + ColorPicker colorPicker = new () + { + Title = "Pick a Border Color", + BorderStyle = LineStyle.Single + }; + + colorPicker.Selecting += (sender, args) => + { + ColorPicker? picker = sender as ColorPicker; + + if (picker is { }) + { + Scheme old = application.TopRunnableView.Border.GetScheme (); + application.TopRunnableView.Border.SetScheme (old with { Normal = new Attribute (picker.SelectedColor, Color.Black) }); + } + args.Handled = true; + }; + + PopoverWrapper popover = colorPicker.AsPopover (); + popover.X = Pos.Center (); + popover.Y = Pos.Center (); + application.Popover?.Register (popover); + application.Popover?.Show (popover); + + e.Handled = true; +}; + +mainWindow.Add (button4); + +// Example 5: Custom position and size +Button button5 = new () +{ + Title = "_5: Positioned Popover", + X = Pos.Center (), + Y = Pos.Bottom (button4) + 1 +}; + +button5.Accepting += (s, e) => +{ + IApplication? application = (s as View)?.App; + + if (application is null) + { + return; + } + + View customView = new () + { + Title = "Custom Position", + X = Pos.Percent (10), + Y = Pos.Percent (10), + Width = Dim.Percent (50), + Height = Dim.Percent (60), + BorderStyle = LineStyle.Double + }; + + customView.Add ( + new Label + { + Text = "This popover has a custom position and size.\n\nYou can set X, Y, Width, and Height\nusing Pos and Dim to position it anywhere.", + X = 2, + Y = 2 + }); + + Button closeButton = new () + { + Title = "Close", + X = Pos.Center (), + Y = Pos.AnchorEnd (1) + }; + + closeButton.Accepting += (sender, args) => + { + if (customView.SuperView is PopoverWrapper wrapper) + { + wrapper.Visible = false; + } + + args.Handled = true; + }; + + customView.Add (closeButton); + + PopoverWrapper popover = customView.AsPopover (); + application.Popover?.Register (popover); + popover.X = Pos.Center (); + popover.Y = Pos.Center (); + application.Popover?.Show (popover); + + e.Handled = true; +}; + +mainWindow.Add (button5); + +// Quit button +Button quitButton = new () +{ + Title = "_Quit", + X = Pos.Center (), + Y = Pos.AnchorEnd (1) +}; + +quitButton.Accepting += (s, e) => +{ + app.RequestStop (); + e.Handled = true; +}; + +mainWindow.Add (quitButton); + +app.Run (mainWindow); +mainWindow.Dispose (); +app.Dispose (); + +// Helper method to create a form view +View CreateFormView (IApplication application) +{ + View form = new () + { + Title = "User Registration Form", + X = Pos.Center (), + Y = Pos.Center (), + Width = Dim.Percent (60), + Height = Dim.Percent (50), + BorderStyle = LineStyle.Single + }; + + Label nameLabel = new () + { + Text = "Name:", + X = 2, + Y = 1 + }; + + TextField nameField = new () + { + X = Pos.Right (nameLabel) + 2, + Y = Pos.Top (nameLabel), + Width = Dim.Fill (2) + }; + + Label emailLabel = new () + { + Text = "Email:", + X = Pos.Left (nameLabel), + Y = Pos.Bottom (nameLabel) + 1 + }; + + TextField emailField = new () + { + X = Pos.Right (emailLabel) + 2, + Y = Pos.Top (emailLabel), + Width = Dim.Fill (2) + }; + + Label ageLabel = new () + { + Text = "Age:", + X = Pos.Left (nameLabel), + Y = Pos.Bottom (emailLabel) + 1 + }; + + TextField ageField = new () + { + X = Pos.Right (ageLabel) + 2, + Y = Pos.Top (ageLabel), + Width = Dim.Percent (20) + }; + + CheckBox agreeCheckbox = new () + { + Title = "I agree to the terms and conditions", + X = Pos.Left (nameLabel), + Y = Pos.Bottom (ageLabel) + 1 + }; + + Button submitButton = new () + { + Title = "Submit", + X = Pos.Center () - 8, + Y = Pos.AnchorEnd (2), + IsDefault = true + }; + + submitButton.Accepting += (s, e) => + { + if (string.IsNullOrWhiteSpace (nameField.Text)) + { + MessageBox.ErrorQuery (application, "Error", "Name is required!", "OK"); + e.Handled = true; + + return; + } + + if (agreeCheckbox.CheckedState != CheckState.Checked) + { + MessageBox.ErrorQuery (application, "Error", "You must agree to the terms!", "OK"); + e.Handled = true; + + return; + } + + MessageBox.Query ( + application, + "Success", + $"Registration submitted!\n\nName: {nameField.Text}\nEmail: {emailField.Text}\nAge: {ageField.Text}", + "OK"); + + if (form.SuperView is PopoverWrapper wrapper) + { + wrapper.Visible = false; + } + + e.Handled = true; + }; + + Button cancelButton = new () + { + Title = "Cancel", + X = Pos.Center () + 4, + Y = Pos.Top (submitButton) + }; + + cancelButton.Accepting += (s, e) => + { + if (form.SuperView is PopoverWrapper wrapper) + { + wrapper.Visible = false; + } + + e.Handled = true; + }; + + form.Add (nameLabel, nameField); + form.Add (emailLabel, emailField); + form.Add (ageLabel, ageField); + form.Add (agreeCheckbox); + form.Add (submitButton, cancelButton); + + return form; +} diff --git a/Examples/PopoverWrapperExample/README.md b/Examples/PopoverWrapperExample/README.md new file mode 100644 index 000000000..2600ebe21 --- /dev/null +++ b/Examples/PopoverWrapperExample/README.md @@ -0,0 +1,108 @@ +# PopoverWrapper Example + +This example demonstrates how to use `PopoverWrapper` to make any View into a popover without implementing the `IPopover` interface. + +## Overview + +`PopoverWrapper` is similar to `RunnableWrapper` but for popovers instead of runnables. It wraps any View and automatically handles: + +- Setting proper viewport settings (transparent, transparent mouse) +- Configuring focus behavior +- Handling the quit command to hide the popover +- Sizing to fill the screen by default + +## Key Features + +- **Fluent API**: Use `.AsPopover()` extension method for a clean, fluent syntax +- **Any View**: Wrap any existing View - Button, ListView, custom Views, forms, etc. +- **Automatic Management**: The wrapper handles all the popover boilerplate +- **Type-Safe**: Generic type parameter ensures type safety when accessing the wrapped view + +## Usage + +### Basic Usage + +```csharp +// Create any view +var myView = new View +{ + X = Pos.Center (), + Y = Pos.Center (), + Width = 40, + Height = 10, + BorderStyle = LineStyle.Single +}; + +// Wrap it as a popover +PopoverWrapper popover = myView.AsPopover (); + +// Register and show +app.Popover.Register (popover); +app.Popover.Show (popover); +``` + +### With ListView + +```csharp +var listView = new ListView +{ + Title = "Select an Item", + X = Pos.Center (), + Y = Pos.Center (), + Width = 30, + Height = 10, + Source = new ListWrapper (["Apple", "Banana", "Cherry"]) +}; + +PopoverWrapper popover = listView.AsPopover (); +app.Popover.Register (popover); +app.Popover.Show (popover); +``` + +### With Custom Forms + +```csharp +View CreateFormView () +{ + var form = new View + { + Title = "User Form", + X = Pos.Center (), + Y = Pos.Center (), + Width = 60, + Height = 16 + }; + + // Add form fields... + + return form; +} + +View formView = CreateFormView (); +PopoverWrapper popover = formView.AsPopover (); +app.Popover.Register (popover); +app.Popover.Show (popover); +``` + +## Comparison with RunnableWrapper + +| Feature | RunnableWrapper | PopoverWrapper | +|---------|----------------|----------------| +| Purpose | Make any View runnable as a modal session | Make any View into a popover | +| Blocking | Yes, blocks until stopped | No, non-blocking overlay | +| Result Extraction | Yes, via typed Result property | N/A (access WrappedView directly) | +| Dismissal | Via RequestStop() or Quit command | Via Quit command or clicking outside | +| Focus | Takes exclusive focus | Shares focus with underlying content | + +## Running the Example + +```bash +dotnet run --project Examples/PopoverWrapperExample +``` + +## See Also + +- [Popovers Deep Dive](../../docfx/docs/Popovers.md) +- [RunnableWrapper Example](../RunnableWrapperExample/) +- `Terminal.Gui.App.PopoverBaseImpl` +- `Terminal.Gui.App.IPopover` diff --git a/Examples/UICatalog/Scenarios/DropDownListExample.cs b/Examples/UICatalog/Scenarios/DropDownListExample.cs index 51f1cc4c1..29615a3bf 100644 --- a/Examples/UICatalog/Scenarios/DropDownListExample.cs +++ b/Examples/UICatalog/Scenarios/DropDownListExample.cs @@ -18,34 +18,11 @@ public sealed class DropDownListExample : Scenario BorderStyle = LineStyle.None }; - Label l = new Label () { Title = "_DropDown:" }; + Label label = new Label () { Title = "_DropDown TextField Using Menu:" }; + View view = CreateDropDownTextFieldUsingMenu (); + view.X = Pos.Right (label) + 1; - TextField tf = new () { X = Pos.Right(l), Width = 10 }; - - MenuBarItem? menuBarItem = new ($"{Glyphs.DownArrow}", - Enumerable.Range (1, 5) - .Select (selector: i => new MenuItem($"item {i}", null, null, null) ) - .ToArray ()); - - var mb = new MenuBar ([menuBarItem]) - { - CanFocus = true, - Width = 1, - Y = Pos.Top (tf), - X = Pos.Right (tf) - }; - - // HACKS required to make this work: - mb.Accepted += (s, e) => { - // BUG: This does not select menu item 0 - // Instead what happens is the first keystroke the user presses - // gets swallowed and focus is moved to 0. Result is that you have - // to press down arrow twice to select first menu item and/or have to - // press Tab twice to move focus back to TextField - mb.OpenMenu (); - }; - - appWindow.Add (l, tf, mb); + appWindow.Add (label, view); // Run - Start the application. Application.Run (appWindow); @@ -54,4 +31,140 @@ public sealed class DropDownListExample : Scenario // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } + + private View CreateDropDownTextFieldUsingMenu () + { + + TextField tf = new () + { + Text = "item 1", + Width = 10, + Height = 1 + }; + + MenuBarItem? menuBarItem = new ($"{Glyphs.DownArrow}", Enumerable.Range (1, 5) + .Select (i => + { + MenuItem item = new MenuItem ($"item {i}", null, null, null); + item.Accepting += (s, e) => + { + tf.Text = item.Title; + //e.Handled = true; + }; + + return item; + }) + .ToArray ()) + { + MarginThickness = Thickness.Empty + }; + + menuBarItem.PopoverMenuOpenChanged += (s, e) => + { + if (e.Value && s is MenuBarItem sender) + { + sender.PopoverMenu!.Root.X = tf.FrameToScreen ().X; + sender.PopoverMenu.Root.Width = tf.Width + sender.Width; + // Find the subview of Root whos Text matches tf.Text and setfocus to it + var menuItemToSelect = sender.PopoverMenu.Root.SubViews.OfType ().FirstOrDefault (mi => mi.Title == tf.Text.ToString ()); + menuItemToSelect?.SetFocus (); + } + }; + + + var mb = new MenuBar ([menuBarItem]) + { + CanFocus = true, + Width = Dim.Auto (), + Y = Pos.Top (tf), + X = Pos.Right (tf) + }; + + // HACKS required to make this work: + mb.Accepted += (s, e) => + { + // BUG: This does not select menu item 0 + // Instead what happens is the first keystroke the user presses + // gets swallowed and focus is moved to 0. Result is that you have + // to press down arrow twice to select first menu item and/or have to + // press Tab twice to move focus back to TextField + mb.OpenMenu (); + }; + + View superView = new () + { + CanFocus = true, + Height = Dim.Auto (), + Width = Dim.Auto() + }; + superView.Add (tf, mb); + + return superView; + + } + + //private View CreateDropDownTextFieldUsingListView () + //{ + // TextField tf = new () + // { + // Text = "item 1", + // Width = 10, + // Height = 1 + // }; + + + // ListView listView = new () + // { + // Source = new ListWrapper (["item 1", "item 2", "item 3", "item 4", "item 5"]), + // }; + + + // MenuBarItem? menuBarItem = new ($"{Glyphs.DownArrow}", ) + // { + // MarginThickness = Thickness.Empty + // }; + + // menuBarItem.PopoverMenuOpenChanged += (s, e) => + // { + // if (e.Value && s is MenuBarItem sender) + // { + // sender.PopoverMenu!.Root.X = tf.FrameToScreen ().X; + // sender.PopoverMenu.Root.Width = tf.Width + sender.Width; + // // Find the subview of Root whos Text matches tf.Text and setfocus to it + // var menuItemToSelect = sender.PopoverMenu.Root.SubViews.OfType ().FirstOrDefault (mi => mi.Title == tf.Text.ToString ()); + // menuItemToSelect?.SetFocus (); + // } + // }; + + + // var mb = new MenuBar ([menuBarItem]) + // { + // CanFocus = true, + // Width = Dim.Auto (), + // Y = Pos.Top (tf), + // X = Pos.Right (tf) + // }; + + // // HACKS required to make this work: + // mb.Accepted += (s, e) => + // { + // // BUG: This does not select menu item 0 + // // Instead what happens is the first keystroke the user presses + // // gets swallowed and focus is moved to 0. Result is that you have + // // to press down arrow twice to select first menu item and/or have to + // // press Tab twice to move focus back to TextField + // mb.OpenMenu (); + // }; + + // View superView = new () + // { + // CanFocus = true, + // Height = Dim.Auto (), + // Width = Dim.Auto () + // }; + // superView.Add (tf, mb); + + // return superView; + + //} } diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs index ff118df35..70eeb4568 100644 --- a/Terminal.Gui/App/PopoverBaseImpl.cs +++ b/Terminal.Gui/App/PopoverBaseImpl.cs @@ -121,8 +121,42 @@ public abstract class PopoverBaseImpl : View, IPopover { App?.Navigation?.SetFocused (App?.TopRunnableView?.MostFocused); } + + App?.TopRunnableView?.SetNeedsDraw (); } return ret; } + + ///// + ///// Locates the popover menu at . The actual position of the menu will be + ///// adjusted to + ///// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the + ///// first MenuItem (if possible). + ///// + ///// If , the current mouse position will be used. + //public void SetPosition (Point? idealScreenPosition = null) + //{ + // idealScreenPosition ??= App?.Mouse.LastMousePosition; + + // if (idealScreenPosition is null) + // { + // return; + // } + + // Point pos = idealScreenPosition.Value; + + // if (!Root.IsInitialized) + // { + // Root.App ??= App; + // Root.BeginInit (); + // Root.EndInit (); + // Root.Layout (); + // } + + // pos = GetMostVisibleLocationForSubMenu (Root, pos); + + // Root.X = pos.X; + // Root.Y = pos.Y; + //} } diff --git a/Terminal.Gui/App/PopoverWrapper.cs b/Terminal.Gui/App/PopoverWrapper.cs new file mode 100644 index 000000000..6ff1890ed --- /dev/null +++ b/Terminal.Gui/App/PopoverWrapper.cs @@ -0,0 +1,95 @@ +namespace Terminal.Gui.App; + +/// +/// Wraps any to make it a popover, similar to how +/// wraps views to make them runnable. +/// +/// The type of view being wrapped. +/// +/// +/// This class enables any View to be shown as a popover with +/// +/// without requiring the View to implement or derive from +/// . +/// +/// +/// The wrapper automatically handles: +/// +/// Setting proper viewport settings (transparent, transparent mouse) +/// Configuring focus behavior +/// Handling the quit command to hide the popover +/// Sizing to fill the screen by default +/// +/// +/// +/// Use for a fluent API approach. +/// +/// +/// +/// // Wrap a custom view to make it a popover +/// var myView = new View +/// { +/// X = Pos.Center (), +/// Y = Pos.Center (), +/// Width = 40, +/// Height = 10, +/// BorderStyle = LineStyle.Single +/// }; +/// myView.Add (new Label { Text = "Hello Popover!" }); +/// +/// var popover = new PopoverWrapper<View> { WrappedView = myView }; +/// app.Popover.Register (popover); +/// app.Popover.Show (popover); +/// +/// +/// +public class PopoverWrapper : PopoverBaseImpl where TView : View +{ + /// + /// Initializes a new instance of . + /// + public PopoverWrapper () + { + Id = "popoverWrapper"; + Width = Dim.Auto (); + Height = Dim.Auto (); + } + + private TView? _wrappedView; + + /// + /// Gets or sets the wrapped view that is being made into a popover. + /// + /// + /// + /// This property must be set before the wrapper is initialized. + /// Access this property to interact with the original view or configure its behavior. + /// + /// + /// Thrown if the property is set after initialization. + public required TView WrappedView + { + get => _wrappedView ?? throw new InvalidOperationException ("WrappedView must be set before use."); + init + { + if (IsInitialized) + { + throw new InvalidOperationException ("WrappedView cannot be changed after initialization."); + } + + _wrappedView = value; + } + } + + /// + public override void EndInit () + { + base.EndInit (); + + // Add the wrapped view as a subview after initialization + if (_wrappedView is { }) + { + Add (_wrappedView); + } + } +} diff --git a/Terminal.Gui/App/ViewPopoverExtensions.cs b/Terminal.Gui/App/ViewPopoverExtensions.cs new file mode 100644 index 000000000..1a25fb4c0 --- /dev/null +++ b/Terminal.Gui/App/ViewPopoverExtensions.cs @@ -0,0 +1,60 @@ +namespace Terminal.Gui.App; + +/// +/// Extension methods for making any into a popover. +/// +/// +/// These extensions provide a fluent API for wrapping views in , +/// enabling any View to be shown as a popover without implementing . +/// +public static class ViewPopoverExtensions +{ + /// + /// Converts any View into a popover. + /// + /// The type of view to make into a popover. + /// The view to wrap. Cannot be null. + /// A that wraps the view. + /// Thrown if is null. + /// + /// + /// This method wraps the view in a which automatically + /// handles popover behavior including transparency, focus, and quit key handling. + /// + /// + /// After creating the wrapper, register it with + /// and show it with . + /// + /// + /// + /// + /// // Make a custom view into a popover + /// var myView = new View + /// { + /// X = Pos.Center (), + /// Y = Pos.Center (), + /// Width = 40, + /// Height = 10, + /// BorderStyle = LineStyle.Single + /// }; + /// myView.Add (new Label { Text = "Hello Popover!" }); + /// + /// var popover = myView.AsPopover(); + /// app.Popover.Register (popover); + /// app.Popover.Show (popover); + /// + /// // The wrapped view can still be accessed + /// Console.WriteLine ($"View id: {popover.WrappedView.Id}"); + /// + /// + public static PopoverWrapper AsPopover (this TView view) + where TView : View + { + if (view is null) + { + throw new ArgumentNullException (nameof (view)); + } + + return new PopoverWrapper { WrappedView = view }; + } +} diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 87d0f9f89..0fd5f41ee 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -190,8 +190,32 @@ public class Shortcut : View, IOrientation, IDesignable SetRelativeLayout (SuperView?.GetContentSize () ?? screenSize); } - // TODO: Enable setting of the margin thickness - private Thickness GetMarginThickness () => new (1, 0, 1, 0); + private Thickness _marginThickness = new (1, 0, 1, 0); + + /// + /// Gets or sets the margin thickness applied to the CommandView, HelpView, and KeyView. + /// The default is (1,0,1,0). + /// + public Thickness MarginThickness + { + get => _marginThickness; + set + { + _marginThickness = value; + if (CommandView.Margin is { }) + { + CommandView.Margin!.Thickness = value; + } + if (HelpView.Margin is { }) + { + HelpView.Margin!.Thickness = value; + } + if (KeyView.Margin is { }) + { + KeyView.Margin!.Thickness = value; + } + } + } // When layout starts, we need to adjust the layout of the HelpView and KeyView /// @@ -212,7 +236,7 @@ public class Shortcut : View, IOrientation, IDesignable if (_maxHelpWidth < 3) { - Thickness t = GetMarginThickness (); + Thickness t = MarginThickness; switch (_maxHelpWidth) { @@ -234,7 +258,7 @@ public class Shortcut : View, IOrientation, IDesignable else { // Reset to default - HelpView.Margin!.Thickness = GetMarginThickness (); + HelpView.Margin!.Thickness = MarginThickness; } } @@ -484,7 +508,7 @@ public class Shortcut : View, IOrientation, IDesignable { if (CommandView.Margin is { }) { - CommandView.Margin!.Thickness = GetMarginThickness (); + CommandView.Margin!.Thickness = MarginThickness; // strip off ViewportSettings.TransparentMouse CommandView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; @@ -550,7 +574,7 @@ public class Shortcut : View, IOrientation, IDesignable { if (HelpView.Margin is { }) { - HelpView.Margin!.Thickness = GetMarginThickness (); + HelpView.Margin!.Thickness = MarginThickness; // strip off ViewportSettings.TransparentMouse HelpView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; @@ -687,7 +711,7 @@ public class Shortcut : View, IOrientation, IDesignable { if (KeyView.Margin is { }) { - KeyView.Margin!.Thickness = GetMarginThickness (); + KeyView.Margin!.Thickness = MarginThickness; // strip off ViewportSettings.TransparentMouse KeyView.Margin!.ViewportSettings &= ~ViewportSettingsFlags.TransparentMouse; diff --git a/Terminal.sln b/Terminal.sln index aacab4c01..74bae5434 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentExample", "Examples\F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunnableWrapperExample", "Examples\RunnableWrapperExample\RunnableWrapperExample.csproj", "{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PopoverWrapperExample", "Examples\PopoverWrapperExample\PopoverWrapperExample.csproj", "{4DFA7371-86D5-B970-9535-368FE1393D90}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -209,6 +211,10 @@ Global {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.Build.0 = Release|Any CPU + {4DFA7371-86D5-B970-9535-368FE1393D90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DFA7371-86D5-B970-9535-368FE1393D90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DFA7371-86D5-B970-9535-368FE1393D90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DFA7371-86D5-B970-9535-368FE1393D90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/UnitTestsParallelizable/Application/PopoverWrapperTests.cs b/Tests/UnitTestsParallelizable/Application/PopoverWrapperTests.cs new file mode 100644 index 000000000..75309d993 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/PopoverWrapperTests.cs @@ -0,0 +1,164 @@ +using Terminal.Gui.App; +using Terminal.Gui.Views; + +namespace ApplicationTests; + +public class PopoverWrapperTests +{ + [Fact] + public void Constructor_SetsDefaults () + { + var wrapper = new PopoverWrapper { WrappedView = new View () }; + + Assert.Equal ("popoverWrapper", wrapper.Id); + Assert.True (wrapper.CanFocus); + Assert.Equal (Dim.Fill (), wrapper.Width); + Assert.Equal (Dim.Fill (), wrapper.Height); + Assert.True (wrapper.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)); + Assert.True (wrapper.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse)); + } + + [Fact] + public void WrappedView_CanBeSet () + { + var view = new View { Id = "testView" }; + var wrapper = new PopoverWrapper { WrappedView = view }; + + Assert.Same (view, wrapper.WrappedView); + Assert.Equal ("testView", wrapper.WrappedView.Id); + } + + [Fact] + public void EndInit_AddsWrappedViewAsSubview () + { + var view = new View { Id = "wrapped" }; + var wrapper = new PopoverWrapper { WrappedView = view }; + + wrapper.BeginInit (); + wrapper.EndInit (); + + Assert.Contains (view, wrapper.SubViews); + Assert.Same (wrapper, view.SuperView); + } + + [Fact] + public void CanBeRegisteredAndShown () + { + var view = new View + { + X = Pos.Center (), + Y = Pos.Center (), + Width = 20, + Height = 10 + }; + + var wrapper = new PopoverWrapper { WrappedView = view }; + var popoverManager = new ApplicationPopover (); + + popoverManager.Register (wrapper); + Assert.Contains (wrapper, popoverManager.Popovers); + + popoverManager.Show (wrapper); + Assert.Equal (wrapper, popoverManager.GetActivePopover ()); + Assert.True (wrapper.Visible); + } + + [Fact] + public void QuitCommand_HidesPopover () + { + var view = new View (); + var wrapper = new PopoverWrapper { WrappedView = view }; + var popoverManager = new ApplicationPopover (); + + popoverManager.Register (wrapper); + popoverManager.Show (wrapper); + + Assert.True (wrapper.Visible); + + wrapper.InvokeCommand (Command.Quit); + + Assert.False (wrapper.Visible); + } + + [Fact] + public void AsPopover_Extension_CreatesWrapper () + { + var view = new View { Id = "testView" }; + + PopoverWrapper wrapper = view.AsPopover (); + + Assert.NotNull (wrapper); + Assert.Same (view, wrapper.WrappedView); + } + + [Fact] + public void AsPopover_Extension_ThrowsIfViewIsNull () + { + View? view = null; + + Assert.Throws (() => view!.AsPopover ()); + } + + [Fact] + public void WrappedView_ReceivesInput () + { + var textField = new TextField { Width = 20 }; + var wrapper = new PopoverWrapper { WrappedView = textField }; + + wrapper.BeginInit (); + wrapper.EndInit (); + + var popoverManager = new ApplicationPopover (); + popoverManager.Register (wrapper); + popoverManager.Show (wrapper); + + Assert.True (wrapper.Visible); + Assert.Contains (textField, wrapper.SubViews); + } + + [Fact] + public void Multiple_Types_CanBeWrapped () + { + var label = new Label { Text = "Test" }; + var labelWrapper = new PopoverWrapper