mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
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:
15
Examples/PopoverWrapperExample/PopoverWrapperExample.csproj
Normal file
15
Examples/PopoverWrapperExample/PopoverWrapperExample.csproj
Normal 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>
|
||||
417
Examples/PopoverWrapperExample/Program.cs
Normal file
417
Examples/PopoverWrapperExample/Program.cs
Normal 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;
|
||||
}
|
||||
108
Examples/PopoverWrapperExample/README.md
Normal file
108
Examples/PopoverWrapperExample/README.md
Normal 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`
|
||||
Reference in New Issue
Block a user