Add PopoverWrapper and enhance DropDownListExample

Refactored `DropDownListExample` for modularity and lifecycle management. Introduced `PopoverWrapper<TView>` to enable any `View` to function as a popover, along with `ViewPopoverExtensions` for a fluent API. Added `PopoverWrapperExample` project to demonstrate usage with examples like `ListView`, forms, and `ColorPicker`.

Enhanced `Shortcut` class with a configurable `MarginThickness` property. Updated `PopoverBaseImpl` to redraw UI on visibility changes. Added comprehensive unit tests for `PopoverWrapper` and extensions. Updated `Terminal.sln` to include the new project. Added detailed documentation in `README.md`.

Improved code maintainability, modularity, and user experience.
This commit is contained in:
Tig
2025-12-06 14:49:57 -07:00
parent a89655408c
commit 01c94ce5b2
10 changed files with 1070 additions and 34 deletions

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<View> 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<string> (["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"]),
SelectedItem = 0
};
PopoverWrapper<ListView> 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<View> 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<ColorPicker> 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<View> wrapper)
{
wrapper.Visible = false;
}
args.Handled = true;
};
customView.Add (closeButton);
PopoverWrapper<View> 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<View> 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<View> 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;
}

View File

@@ -0,0 +1,108 @@
# PopoverWrapper Example
This example demonstrates how to use `PopoverWrapper<TView>` to make any View into a popover without implementing the `IPopover` interface.
## Overview
`PopoverWrapper<TView>` is similar to `RunnableWrapper<TView, TResult>` 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<View> 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<string> (["Apple", "Banana", "Cherry"])
};
PopoverWrapper<ListView> 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<View> 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`

View File

@@ -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<MenuItem> ().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<string> (["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<MenuItem> ().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;
//}
}

View File

@@ -121,8 +121,42 @@ public abstract class PopoverBaseImpl : View, IPopover
{
App?.Navigation?.SetFocused (App?.TopRunnableView?.MostFocused);
}
App?.TopRunnableView?.SetNeedsDraw ();
}
return ret;
}
///// <summary>
///// Locates the popover menu at <paramref name="idealScreenPosition"/>. 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).
///// </summary>
///// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
//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;
//}
}

View File

@@ -0,0 +1,95 @@
namespace Terminal.Gui.App;
/// <summary>
/// Wraps any <see cref="View"/> to make it a popover, similar to how
/// <see cref="RunnableWrapper{TView, TResult}"/> wraps views to make them runnable.
/// </summary>
/// <typeparam name="TView">The type of view being wrapped.</typeparam>
/// <remarks>
/// <para>
/// This class enables any View to be shown as a popover with
/// <see cref="ApplicationPopover.Show"/>
/// without requiring the View to implement <see cref="IPopover"/> or derive from
/// <see cref="PopoverBaseImpl"/>.
/// </para>
/// <para>
/// The wrapper automatically handles:
/// <list type="bullet">
/// <item>Setting proper viewport settings (transparent, transparent mouse)</item>
/// <item>Configuring focus behavior</item>
/// <item>Handling the quit command to hide the popover</item>
/// <item>Sizing to fill the screen by default</item>
/// </list>
/// </para>
/// <para>
/// Use <see cref="ViewPopoverExtensions.AsPopover{TView}"/> for a fluent API approach.
/// </para>
/// <example>
/// <code>
/// // 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&lt;View&gt; { WrappedView = myView };
/// app.Popover.Register (popover);
/// app.Popover.Show (popover);
/// </code>
/// </example>
/// </remarks>
public class PopoverWrapper<TView> : PopoverBaseImpl where TView : View
{
/// <summary>
/// Initializes a new instance of <see cref="PopoverWrapper{TView}"/>.
/// </summary>
public PopoverWrapper ()
{
Id = "popoverWrapper";
Width = Dim.Auto ();
Height = Dim.Auto ();
}
private TView? _wrappedView;
/// <summary>
/// Gets or sets the wrapped view that is being made into a popover.
/// </summary>
/// <remarks>
/// <para>
/// This property must be set before the wrapper is initialized.
/// Access this property to interact with the original view or configure its behavior.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown if the property is set after initialization.</exception>
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;
}
}
/// <inheritdoc/>
public override void EndInit ()
{
base.EndInit ();
// Add the wrapped view as a subview after initialization
if (_wrappedView is { })
{
Add (_wrappedView);
}
}
}

View File

@@ -0,0 +1,60 @@
namespace Terminal.Gui.App;
/// <summary>
/// Extension methods for making any <see cref="View"/> into a popover.
/// </summary>
/// <remarks>
/// These extensions provide a fluent API for wrapping views in <see cref="PopoverWrapper{TView}"/>,
/// enabling any View to be shown as a popover without implementing <see cref="IPopover"/>.
/// </remarks>
public static class ViewPopoverExtensions
{
/// <summary>
/// Converts any View into a popover.
/// </summary>
/// <typeparam name="TView">The type of view to make into a popover.</typeparam>
/// <param name="view">The view to wrap. Cannot be null.</param>
/// <returns>A <see cref="PopoverWrapper{TView}"/> that wraps the view.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="view"/> is null.</exception>
/// <remarks>
/// <para>
/// This method wraps the view in a <see cref="PopoverWrapper{TView}"/> which automatically
/// handles popover behavior including transparency, focus, and quit key handling.
/// </para>
/// <para>
/// After creating the wrapper, register it with <see cref="ApplicationPopover.Register"/>
/// and show it with <see cref="ApplicationPopover.Show"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // 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}");
/// </code>
/// </example>
public static PopoverWrapper<TView> AsPopover<TView> (this TView view)
where TView : View
{
if (view is null)
{
throw new ArgumentNullException (nameof (view));
}
return new PopoverWrapper<TView> { WrappedView = view };
}
}

View File

@@ -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);
/// <summary>
/// Gets or sets the margin thickness applied to the CommandView, HelpView, and KeyView.
/// The default is (1,0,1,0).
/// </summary>
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
/// <inheritdoc/>
@@ -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;

View File

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

View File

@@ -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<View> { 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<View> { 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<View> { 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<View> { 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<View> { 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<View> wrapper = view.AsPopover ();
Assert.NotNull (wrapper);
Assert.Same (view, wrapper.WrappedView);
}
[Fact]
public void AsPopover_Extension_ThrowsIfViewIsNull ()
{
View? view = null;
Assert.Throws<ArgumentNullException> (() => view!.AsPopover ());
}
[Fact]
public void WrappedView_ReceivesInput ()
{
var textField = new TextField { Width = 20 };
var wrapper = new PopoverWrapper<TextField> { 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<Label> { WrappedView = label };
var button = new Button { Title = "Click" };
var buttonWrapper = new PopoverWrapper<Button> { WrappedView = button };
var listView = new ListView ();
var listViewWrapper = new PopoverWrapper<ListView> { WrappedView = listView };
Assert.Same (label, labelWrapper.WrappedView);
Assert.Same (button, buttonWrapper.WrappedView);
Assert.Same (listView, listViewWrapper.WrappedView);
}
[Fact]
public void Current_Property_CanBeSetAndGet ()
{
var view = new View ();
var wrapper = new PopoverWrapper<View> { WrappedView = view };
var runnable = new Runnable ();
wrapper.Current = runnable;
Assert.Same (runnable, wrapper.Current);
}
[Fact]
public void Disposed_Wrapper_DisposesWrappedView ()
{
var view = new View ();
var wrapper = new PopoverWrapper<View> { WrappedView = view };
wrapper.BeginInit ();
wrapper.EndInit ();
bool viewDisposed = false;
view.Disposing += (s, e) => viewDisposed = true;
wrapper.Dispose ();
Assert.True (viewDisposed);
}
}