diff --git a/CommunityToolkitExample/CommunityToolkitExample.csproj b/CommunityToolkitExample/CommunityToolkitExample.csproj
new file mode 100644
index 000000000..fdb41dfcd
--- /dev/null
+++ b/CommunityToolkitExample/CommunityToolkitExample.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CommunityToolkitExample/LoginActions.cs b/CommunityToolkitExample/LoginActions.cs
new file mode 100644
index 000000000..3cbc6972c
--- /dev/null
+++ b/CommunityToolkitExample/LoginActions.cs
@@ -0,0 +1,8 @@
+namespace CommunityToolkitExample;
+
+internal enum LoginActions
+{
+ Clear,
+ Validation,
+ LoginProgress
+}
diff --git a/CommunityToolkitExample/LoginView.Designer.cs b/CommunityToolkitExample/LoginView.Designer.cs
new file mode 100644
index 000000000..e1bddff45
--- /dev/null
+++ b/CommunityToolkitExample/LoginView.Designer.cs
@@ -0,0 +1,62 @@
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginView : Window
+{
+ private Label titleLabel;
+ private Label usernameLengthLabel;
+ private TextField usernameInput;
+ private Label passwordLengthLabel;
+ private TextField passwordInput;
+ private Label validationLabel;
+ private Button loginButton;
+ private Button clearButton;
+ private Label loginProgressLabel;
+
+ private void InitializeComponent ()
+ {
+ titleLabel = new Label ();
+ titleLabel.Text = "Login Form";
+ Add (titleLabel);
+ usernameLengthLabel = new Label ();
+ usernameLengthLabel.X = Pos.Left (titleLabel);
+ usernameLengthLabel.Y = Pos.Top (titleLabel) + 1;
+ Add (usernameLengthLabel);
+ usernameInput = new TextField ();
+ usernameInput.X = Pos.Right (usernameLengthLabel) + 1;
+ usernameInput.Y = Pos.Top (usernameLengthLabel);
+ usernameInput.Width = 40;
+ Add (usernameInput);
+ passwordLengthLabel = new Label ();
+ passwordLengthLabel.X = Pos.Left (usernameLengthLabel);
+ passwordLengthLabel.Y = Pos.Top (usernameLengthLabel) + 1;
+ Add (passwordLengthLabel);
+ passwordInput = new TextField ();
+ passwordInput.X = Pos.Right (passwordLengthLabel) + 1;
+ passwordInput.Y = Pos.Top (passwordLengthLabel);
+ passwordInput.Width = 40;
+ passwordInput.Secret = true;
+ Add (passwordInput);
+ validationLabel = new Label ();
+ validationLabel.X = Pos.Left (passwordInput);
+ validationLabel.Y = Pos.Top (passwordInput) + 1;
+ Add (validationLabel);
+ loginButton = new Button ();
+ loginButton.X = Pos.Left (validationLabel);
+ loginButton.Y = Pos.Top (validationLabel) + 1;
+ loginButton.Text = "_Login";
+ Add (loginButton);
+ clearButton = new Button ();
+ clearButton.X = Pos.Left (loginButton);
+ clearButton.Y = Pos.Top (loginButton) + 1;
+ clearButton.Text = "_Clear";
+ Add (clearButton);
+ loginProgressLabel = new Label ();
+ loginProgressLabel.X = Pos.Left (clearButton);
+ loginProgressLabel.Y = Pos.Top (clearButton) + 1;
+ loginProgressLabel.Width = 40;
+ loginProgressLabel.Height = 1;
+ Add (loginProgressLabel);
+ }
+}
diff --git a/CommunityToolkitExample/LoginView.cs b/CommunityToolkitExample/LoginView.cs
new file mode 100644
index 000000000..b0d891fa9
--- /dev/null
+++ b/CommunityToolkitExample/LoginView.cs
@@ -0,0 +1,72 @@
+using CommunityToolkit.Mvvm.Messaging;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginView : IRecipient>
+{
+ public LoginView (LoginViewModel viewModel)
+ {
+ WeakReferenceMessenger.Default.Register (this);
+ Title = $"Community Toolkit MVVM Example - {Application.QuitKey} to Exit";
+ ViewModel = viewModel;
+ InitializeComponent ();
+ usernameInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Username = usernameInput.Text;
+ };
+ passwordInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Password = passwordInput.Text;
+ };
+ loginButton.Accept += (_, _) =>
+ {
+ if (!ViewModel.CanLogin) { return; }
+ ViewModel.LoginCommand.Execute (null);
+ };
+
+ clearButton.Accept += (_, _) =>
+ {
+ ViewModel.ClearCommand.Execute (null);
+ };
+
+ Initialized += (_, _) => { ViewModel.Initialized (); };
+ }
+
+ public LoginViewModel ViewModel { get; set; }
+
+ public void Receive (Message message)
+ {
+ switch (message.Value)
+ {
+ case LoginActions.Clear:
+ {
+ loginProgressLabel.Text = ViewModel.LoginProgressMessage;
+ validationLabel.Text = ViewModel.ValidationMessage;
+ validationLabel.ColorScheme = ViewModel.ValidationColorScheme;
+ break;
+ }
+ case LoginActions.LoginProgress:
+ {
+ loginProgressLabel.Text = ViewModel.LoginProgressMessage;
+ break;
+ }
+ case LoginActions.Validation:
+ {
+ validationLabel.Text = ViewModel.ValidationMessage;
+ validationLabel.ColorScheme = ViewModel.ValidationColorScheme;
+ break;
+ }
+ }
+ SetText();
+ Application.Refresh ();
+ }
+
+ private void SetText ()
+ {
+ usernameInput.Text = ViewModel.Username;
+ usernameLengthLabel.Text = ViewModel.UsernameLengthMessage;
+ passwordInput.Text = ViewModel.Password;
+ passwordLengthLabel.Text = ViewModel.PasswordLengthMessage;
+ }
+}
\ No newline at end of file
diff --git a/CommunityToolkitExample/LoginViewModel.cs b/CommunityToolkitExample/LoginViewModel.cs
new file mode 100644
index 000000000..8d48546bb
--- /dev/null
+++ b/CommunityToolkitExample/LoginViewModel.cs
@@ -0,0 +1,128 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginViewModel : ObservableObject
+{
+ private const string DEFAULT_LOGIN_PROGRESS_MESSAGE = "Press 'Login' to log in.";
+ private const string INVALID_LOGIN_MESSAGE = "Please enter a valid user name and password.";
+ private const string LOGGING_IN_PROGRESS_MESSAGE = "Logging in...";
+ private const string VALID_LOGIN_MESSAGE = "The input is valid!";
+
+ [ObservableProperty]
+ private bool _canLogin;
+
+ [ObservableProperty]
+ private string _loginProgressMessage;
+
+ private string _password;
+
+ [ObservableProperty]
+ private string _passwordLengthMessage;
+
+ private string _username;
+
+ [ObservableProperty]
+ private string _usernameLengthMessage;
+
+ [ObservableProperty]
+ private ColorScheme? _validationColorScheme;
+
+ [ObservableProperty]
+ private string _validationMessage;
+ public LoginViewModel ()
+ {
+ _loginProgressMessage = string.Empty;
+ _password = string.Empty;
+ _passwordLengthMessage = string.Empty;
+ _username = string.Empty;
+ _usernameLengthMessage = string.Empty;
+ _validationMessage = string.Empty;
+
+ Username = string.Empty;
+ Password = string.Empty;
+
+ ClearCommand = new (Clear);
+ LoginCommand = new (Execute);
+
+ Clear ();
+
+ return;
+
+ async void Execute () { await Login (); }
+ }
+
+ public RelayCommand ClearCommand { get; }
+
+ public RelayCommand LoginCommand { get; }
+
+ public string Password
+ {
+ get => _password;
+ set
+ {
+ SetProperty (ref _password, value);
+ PasswordLengthMessage = $"_Password ({_password.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+
+ public string Username
+ {
+ get => _username;
+ set
+ {
+ SetProperty (ref _username, value);
+ UsernameLengthMessage = $"_Username ({_username.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+
+ public void Initialized ()
+ {
+ Clear ();
+ }
+
+ private void Clear ()
+ {
+ Username = string.Empty;
+ Password = string.Empty;
+ SendMessage (LoginActions.Clear, DEFAULT_LOGIN_PROGRESS_MESSAGE);
+ }
+
+ private async Task Login ()
+ {
+ SendMessage (LoginActions.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE);
+ await Task.Delay (TimeSpan.FromSeconds (1));
+ Clear ();
+ }
+
+ private void SendMessage (LoginActions loginAction, string message = "")
+ {
+ switch (loginAction)
+ {
+ case LoginActions.Clear:
+ LoginProgressMessage = message;
+ ValidationMessage = INVALID_LOGIN_MESSAGE;
+ ValidationColorScheme = Colors.ColorSchemes ["Error"];
+ break;
+ case LoginActions.LoginProgress:
+ LoginProgressMessage = message;
+ break;
+ case LoginActions.Validation:
+ ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE;
+ ValidationColorScheme = CanLogin ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"];
+ break;
+ }
+ WeakReferenceMessenger.Default.Send (new Message { Value = loginAction });
+ }
+
+ private void ValidateLogin ()
+ {
+ CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
+ SendMessage (LoginActions.Validation);
+ }
+}
diff --git a/CommunityToolkitExample/Message.cs b/CommunityToolkitExample/Message.cs
new file mode 100644
index 000000000..f0e8ad530
--- /dev/null
+++ b/CommunityToolkitExample/Message.cs
@@ -0,0 +1,6 @@
+namespace CommunityToolkitExample;
+
+internal class Message
+{
+ public T? Value { get; set; }
+}
diff --git a/CommunityToolkitExample/Program.cs b/CommunityToolkitExample/Program.cs
new file mode 100644
index 000000000..0d4f21c30
--- /dev/null
+++ b/CommunityToolkitExample/Program.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+public static class Program
+{
+ public static IServiceProvider? Services { get; private set; }
+
+ private static void Main (string [] args)
+ {
+ Services = ConfigureServices ();
+ Application.Init ();
+ Application.Run (Services.GetRequiredService ());
+ Application.Top.Dispose();
+ Application.Shutdown ();
+ }
+
+ private static IServiceProvider ConfigureServices ()
+ {
+ var services = new ServiceCollection ();
+ services.AddTransient ();
+ services.AddTransient ();
+ return services.BuildServiceProvider ();
+ }
+}
\ No newline at end of file
diff --git a/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md
new file mode 100644
index 000000000..c3a712868
--- /dev/null
+++ b/CommunityToolkitExample/README.md
@@ -0,0 +1,154 @@
+# CommunityToolkit.MVVM Example
+
+This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient` in conjunction with `Microsoft.Extensions.DependencyInjection`.
+
+Right away we use IoC to load our views and view models.
+
+``` csharp
+// As a public property for access further in the application if needed.
+public static IServiceProvider Services { get; private set; }
+...
+// In Main
+Services = ConfigureServices ();
+...
+private static IServiceProvider ConfigureServices ()
+{
+ var services = new ServiceCollection ();
+ services.AddTransient ();
+ services.AddTransient ();
+ return services.BuildServiceProvider ();
+}
+```
+
+Now, we start the app and get our main view.
+
+``` csharp
+Application.Run (Services.GetRequiredService ());
+```
+
+Our view implements `IRecipient` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.
+
+``` csharp
+internal partial class LoginView : IRecipient>
+{
+ public LoginView (LoginViewModel viewModel)
+ {
+ // Initialize our Receive method
+ WeakReferenceMessenger.Default.Register (this);
+ ...
+ ViewModel = viewModel;
+ ...
+ passwordInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Password = passwordInput.Text;
+ SetText ();
+ };
+ loginButton.Accept += (_, _) =>
+ {
+ if (!ViewModel.CanLogin) { return; }
+ ViewModel.LoginCommand.Execute (null);
+ };
+ ...
+ // Let the view model know the view is intialized.
+ Initialized += (_, _) => { ViewModel.Initialized (); };
+ }
+ ...
+}
+```
+
+Momentarily slipping over to the view model, all bindable properties use some form of `ObservableProperty` with the class deriving from `ObservableObject`. Commands are of the `RelayCommand` type. The use of `ObservableProperty` generates the code for handling `INotifyPropertyChanged` and `INotifyPropertyChanging`.
+
+``` csharp
+internal partial class LoginViewModel : ObservableObject
+{
+ ...
+ [ObservableProperty]
+ private bool _canLogin;
+
+ private string _password;
+ ...
+ public LoginViewModel ()
+ {
+ ...
+ Password = string.Empty;
+ ...
+ LoginCommand = new (Execute);
+
+ Clear ();
+
+ return;
+
+ async void Execute () { await Login (); }
+ }
+ ...
+ public RelayCommand LoginCommand { get; }
+
+ public string Password
+ {
+ get => _password;
+ set
+ {
+ SetProperty (ref _password, value);
+ PasswordLengthMessage = $"_Password ({_password.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+```
+
+The use of `WeakReferenceMessenger` provides one method of signaling the view from the view model. It's just one way to handle cross-thread messaging in this framework.
+
+``` csharp
+...
+private async Task Login ()
+{
+ SendMessage (LoginAction.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE);
+ await Task.Delay (TimeSpan.FromSeconds (1));
+ Clear ();
+}
+
+private void SendMessage (LoginAction loginAction, string message = "")
+{
+ switch (loginAction)
+ {
+ case LoginAction.LoginProgress:
+ LoginProgressMessage = message;
+ break;
+ case LoginAction.Validation:
+ ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE;
+ ValidationColorScheme = CanLogin ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"];
+ break;
+ }
+ WeakReferenceMessenger.Default.Send (new Message { Value = loginAction });
+}
+
+private void ValidateLogin ()
+{
+ CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
+ SendMessage (LoginAction.Validation);
+}
+...
+```
+
+And the view's `Receive` function which provides an `Application.Refresh()` call to update the UI immediately.
+
+``` csharp
+public void Receive (Message message)
+{
+ switch (message.Value)
+ {
+ case LoginAction.LoginProgress:
+ {
+ loginProgressLabel.Text = ViewModel.LoginProgressMessage;
+ break;
+ }
+ case LoginAction.Validation:
+ {
+ validationLabel.Text = ViewModel.ValidationMessage;
+ validationLabel.ColorScheme = ViewModel.ValidationColorScheme;
+ break;
+ }
+ }
+ SetText();
+ Application.Refresh ();
+}
+```
diff --git a/ReactiveExample/Program.cs b/ReactiveExample/Program.cs
index 2b5105b8c..2bbc6667b 100644
--- a/ReactiveExample/Program.cs
+++ b/ReactiveExample/Program.cs
@@ -12,6 +12,7 @@ public static class Program
RxApp.MainThreadScheduler = TerminalScheduler.Default;
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
Application.Run (new LoginView (new LoginViewModel ()));
+ Application.Top.Dispose();
Application.Shutdown ();
}
}
diff --git a/Scripts/Terminal.Gui.PowerShell.Analyzers.psd1 b/Scripts/Terminal.Gui.PowerShell.Analyzers.psd1
index b5eced04c..c94a3e242 100644
--- a/Scripts/Terminal.Gui.PowerShell.Analyzers.psd1
+++ b/Scripts/Terminal.Gui.PowerShell.Analyzers.psd1
@@ -42,7 +42,7 @@ PowerShellHostName = 'ConsoleHost'
# PowerShellHostVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
-ProcessorArchitecture = 'Amd64'
+ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @('Microsoft.PowerShell.Management','Microsoft.PowerShell.Utility','./Terminal.Gui.PowerShell.Core.psd1')
diff --git a/Scripts/Terminal.Gui.PowerShell.Build.psd1 b/Scripts/Terminal.Gui.PowerShell.Build.psd1
index 9f367487a..97395c3a8 100644
--- a/Scripts/Terminal.Gui.PowerShell.Build.psd1
+++ b/Scripts/Terminal.Gui.PowerShell.Build.psd1
@@ -36,7 +36,7 @@ PowerShellHostVersion = '7.4.0'
# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only.
# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else.
# Has nothing to do with runtime use of Terminal.Gui.
-ProcessorArchitecture = 'Amd64'
+ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
diff --git a/Scripts/Terminal.Gui.PowerShell.Core.psd1 b/Scripts/Terminal.Gui.PowerShell.Core.psd1
index 302005999..541265ee8 100644
--- a/Scripts/Terminal.Gui.PowerShell.Core.psd1
+++ b/Scripts/Terminal.Gui.PowerShell.Core.psd1
@@ -44,7 +44,7 @@ PowerShellHostVersion = '7.4.0'
# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only.
# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else.
# Has nothing to do with runtime use of Terminal.Gui.
-ProcessorArchitecture = 'Amd64'
+ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
diff --git a/Scripts/Terminal.Gui.PowerShell.Git.psd1 b/Scripts/Terminal.Gui.PowerShell.Git.psd1
index 1bfcd58a1..afca6f69b 100644
--- a/Scripts/Terminal.Gui.PowerShell.Git.psd1
+++ b/Scripts/Terminal.Gui.PowerShell.Git.psd1
@@ -44,7 +44,7 @@ PowerShellHostVersion = '7.4.0'
# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only.
# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else.
# Has nothing to do with runtime use of Terminal.Gui.
-ProcessorArchitecture = 'AMD64'
+ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
diff --git a/Scripts/Terminal.Gui.PowerShell.psd1 b/Scripts/Terminal.Gui.PowerShell.psd1
index d90db54d9..ca182f375 100644
--- a/Scripts/Terminal.Gui.PowerShell.psd1
+++ b/Scripts/Terminal.Gui.PowerShell.psd1
@@ -49,7 +49,7 @@ PowerShellHostVersion = '7.4.0'
# Processor architecture (None, MSIL, X86, IA64, Amd64, Arm, or an empty string) required by this module. One value only.
# Set to AMD64 here because development on Terminal.Gui isn't really supported on anything else.
# Has nothing to do with runtime use of Terminal.Gui.
-ProcessorArchitecture = 'Amd64'
+ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs
index f015065fe..6656f8457 100644
--- a/Terminal.Gui/Application/Application.cs
+++ b/Terminal.Gui/Application/Application.cs
@@ -1,7 +1,6 @@
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
-using System.Text.Json.Serialization;
namespace Terminal.Gui;
@@ -77,7 +76,7 @@ public static partial class Application
// this in a function like this ensures we don't make mistakes in
// guaranteeing that the state of this singleton is deterministic when Init
// starts running and after Shutdown returns.
- internal static void ResetState ()
+ internal static void ResetState (bool ignoreDisposed = false)
{
// Shutdown is the bookend for Init. As such it needs to clean up all resources
// Init created. Apps that do any threading will need to code defensively for this.
@@ -85,11 +84,6 @@ public static partial class Application
foreach (Toplevel t in _topLevels)
{
t.Running = false;
-#if DEBUG_IDISPOSABLE
-
- // Don't dispose the toplevels. It's up to caller dispose them
- //Debug.Assert (t.WasDisposed);
-#endif
}
_topLevels.Clear ();
@@ -97,7 +91,7 @@ public static partial class Application
#if DEBUG_IDISPOSABLE
// Don't dispose the Top. It's up to caller dispose it
- if (Top is { })
+ if (!ignoreDisposed && Top is { })
{
Debug.Assert (Top.WasDisposed);
@@ -178,15 +172,15 @@ public static partial class Application
///
///
/// must be called when the application is closing (typically after
- /// has returned) to ensure resources are cleaned up and
+ /// has returned) to ensure resources are cleaned up and
/// terminal settings
/// restored.
///
///
- /// The function combines
- /// and
+ /// The function combines
+ /// and
/// into a single
- /// call. An application cam use without explicitly calling
+ /// call. An application cam use without explicitly calling
/// .
///
///
@@ -314,6 +308,7 @@ public static partial class Application
SupportedCultures = GetSupportedCultures ();
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
_initialized = true;
+ InitializedChanged?.Invoke (null, new (false, _initialized));
}
private static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
@@ -345,7 +340,7 @@ public static partial class Application
/// Shutdown an application initialized with .
///
/// Shutdown must be called for every call to or
- /// to ensure all resources are cleaned
+ /// to ensure all resources are cleaned
/// up (Disposed)
/// and terminal settings are restored.
///
@@ -354,8 +349,17 @@ public static partial class Application
// TODO: Throw an exception if Init hasn't been called.
ResetState ();
PrintJsonErrors ();
+ InitializedChanged?.Invoke (null, new (true, _initialized));
}
+ ///
+ /// This event is fired after the and methods have been called.
+ ///
+ ///
+ /// Intended to support unit tests that need to know when the application has been initialized.
+ ///
+ public static event EventHandler> InitializedChanged;
+
#endregion Initialization (Init/Shutdown)
#region Run (Begin, Run, End, Stop)
@@ -531,6 +535,7 @@ public static partial class Application
toplevel.LayoutSubviews ();
toplevel.PositionToplevels ();
toplevel.FocusFirst ();
+ BringOverlappedTopToFront ();
if (refreshDriver)
{
@@ -640,7 +645,7 @@ public static partial class Application
///
/// Runs the application by creating a object and calling
- /// .
+ /// .
///
///
/// Calling first is not needed as this function will initialize the application.
@@ -657,7 +662,7 @@ public static partial class Application
///
/// Runs the application by creating a -derived object of type T and calling
- /// .
+ /// .
///
///
/// Calling first is not needed as this function will initialize the application.
@@ -677,11 +682,17 @@ public static partial class Application
///
/// The created T object. The caller is responsible for disposing this object.
public static T Run (Func errorHandler = null, ConsoleDriver driver = null)
- where T : Toplevel, new ()
+ where T : Toplevel, new()
{
+ if (!_initialized)
+ {
+ // Init() has NOT been called.
+ InternalInit (driver, null, true);
+ }
+
var top = new T ();
- Run (top, errorHandler, driver);
+ Run (top, errorHandler);
return top;
}
@@ -693,11 +704,11 @@ public static partial class Application
/// modal s such as boxes.
///
///
- /// To make a stop execution, call
+ /// To make a stop execution, call
/// .
///
///
- /// Calling is equivalent to calling
+ /// Calling is equivalent to calling
/// , followed by , and then calling
/// .
///
@@ -708,7 +719,10 @@ public static partial class Application
/// method will only process any pending events, timers, idle handlers and then
/// return control immediately.
///
- /// Calling first is not needed as this function will initialize the application.
+ /// When using or
+ ///
+ /// will be called automatically.
+ ///
///
/// RELEASE builds only: When is any exceptions will be
/// rethrown. Otherwise, if will be called. If
@@ -721,12 +735,7 @@ public static partial class Application
/// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true,
/// rethrows when null).
///
- ///
- /// The to use. If not specified the default driver for the platform will
- /// be used ( , , or ). Must be
- /// if was called.
- ///
- public static void Run (Toplevel view, Func errorHandler = null, ConsoleDriver driver = null)
+ public static void Run (Toplevel view, Func errorHandler = null)
{
ArgumentNullException.ThrowIfNull (view);
@@ -746,7 +755,9 @@ public static partial class Application
else
{
// Init() has NOT been called.
- InternalInit (driver, null, true);
+ throw new InvalidOperationException (
+ "Init() has not been called. Only Run() or Run() can be used without calling Init()."
+ );
}
var resume = true;
@@ -991,7 +1002,7 @@ public static partial class Application
/// Stops the provided , causing or the if provided.
/// The to stop.
///
- /// This will cause to return.
+ /// This will cause to return.
///
/// Calling is equivalent to setting the
/// property on the currently running to false.
diff --git a/Terminal.Gui/Application/ApplicationKeyboard.cs b/Terminal.Gui/Application/ApplicationKeyboard.cs
index 0a56ce712..be737968f 100644
--- a/Terminal.Gui/Application/ApplicationKeyboard.cs
+++ b/Terminal.Gui/Application/ApplicationKeyboard.cs
@@ -141,11 +141,16 @@ partial class Application
{
foreach (View view in binding.Value)
{
- bool? handled = view?.OnInvokingKeyBindings (keyEvent);
-
- if (handled != null && (bool)handled)
+ if (view is {} && view.KeyBindings.TryGet (binding.Key, (KeyBindingScope)0xFFFF, out KeyBinding kb))
{
- return true;
+ //bool? handled = view.InvokeCommands (kb.Commands, binding.Key, kb);
+ bool? handled = view?.OnInvokingKeyBindings (keyEvent, kb.Scope);
+
+ if (handled != null && (bool)handled)
+ {
+ return true;
+ }
+
}
}
}
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
index 331580083..8043c65eb 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
@@ -859,13 +859,24 @@ public static class ConsoleKeyMapping
case ConsoleKey.F24:
keyCode = KeyCode.F24;
+ break;
+ case ConsoleKey.Clear:
+ keyCode = KeyCode.Clear;
+
break;
case ConsoleKey.Tab:
keyCode = KeyCode.Tab;
break;
default:
- keyCode = (KeyCode)consoleKeyInfo.KeyChar;
+ if ((int)consoleKeyInfo.KeyChar is >= 1 and <= 26)
+ {
+ keyCode = (KeyCode)(consoleKeyInfo.KeyChar + 64);
+ }
+ else
+ {
+ keyCode = (KeyCode)consoleKeyInfo.KeyChar;
+ }
break;
}
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 25725b21e..14d71c637 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -515,9 +515,10 @@ internal class CursesDriver : ConsoleDriver
{
// The ESC-number handling, debatable.
// Simulates the AltMask itself by pressing Alt + Space.
+ // Needed for macOS
if (wch2 == (int)KeyCode.Space)
{
- k = KeyCode.AltMask;
+ k = KeyCode.AltMask | KeyCode.Space;
}
else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A
&& wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z)
@@ -532,41 +533,51 @@ internal class CursesDriver : ConsoleDriver
{
k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
}
- else if (wch2 == Curses.KeyCSI)
+ else
{
ConsoleKeyInfo [] cki =
- {
- new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
- };
+ [
+ new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
+ ];
HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
return;
}
- else
- {
- // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
- if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
- {
- k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
- }
+ //else if (wch2 == Curses.KeyCSI)
+ //{
+ // ConsoleKeyInfo [] cki =
+ // {
+ // new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
+ // };
+ // HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
- if (wch2 == 0)
- {
- k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
- }
- else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
- {
- k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
- }
- else if (wch2 < 256)
- {
- k = (KeyCode)wch2; // | KeyCode.AltMask;
- }
- else
- {
- k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
- }
- }
+ // return;
+ //}
+ //else
+ //{
+ // // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
+ // if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
+ // {
+ // k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
+ // }
+
+ // if (wch2 == 0)
+ // {
+ // k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
+ // }
+ // //else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
+ // //{
+ // // k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
+ // //}
+ // else if (wch2 < 256)
+ // {
+ // k = (KeyCode)wch2; // | KeyCode.AltMask;
+ // }
+ // else
+ // {
+ // k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
+ // }
+ //}
key = new Key (k);
}
@@ -584,6 +595,13 @@ internal class CursesDriver : ConsoleDriver
OnKeyDown (new Key (k));
OnKeyUp (new Key (k));
}
+ else if (wch == 127)
+ {
+ // Backspace needed for macOS
+ k = KeyCode.Backspace;
+ OnKeyDown (new Key (k));
+ OnKeyUp (new Key (k));
+ }
else
{
// Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
@@ -611,7 +629,8 @@ internal class CursesDriver : ConsoleDriver
}
// Strip the KeyCode.Space flag off if it's set
- if (k != KeyCode.Space && k.HasFlag (KeyCode.Space))
+ //if (k != KeyCode.Space && k.HasFlag (KeyCode.Space))
+ if (Key.GetIsKeyCodeAtoZ (k) && (k & KeyCode.Space) != 0)
{
k &= ~KeyCode.Space;
}
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
index 0b9d358cb..5700b779f 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
@@ -106,7 +106,6 @@ public partial class Curses
public const int KeyPPage = 0x153;
public const int KeyHome = 0x106;
public const int KeyMouse = 0x199;
- public const int KeyCSI = 0x5b;
public const int KeyEnd = 0x168;
public const int KeyDeleteChar = 0x14a;
public const int KeyInsertChar = 0x14b;
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
index 7c334b5d2..f9c547af6 100644
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
+++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
@@ -195,6 +195,7 @@ public static class EscSeqUtils
buttonState = new List { 0 };
pos = default (Point);
isResponse = false;
+ char keyChar = '\0';
switch (c1Control)
{
@@ -242,10 +243,10 @@ public static class EscSeqUtils
break;
case "SS3":
- key = GetConsoleKey (terminator [0], values [0], ref mod);
+ key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar);
newConsoleKeyInfo = new ConsoleKeyInfo (
- '\0',
+ keyChar,
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
@@ -271,7 +272,7 @@ public static class EscSeqUtils
if (!string.IsNullOrEmpty (terminator))
{
- key = GetConsoleKey (terminator [0], values [0], ref mod);
+ key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar);
if (key != 0 && values.Length > 1)
{
@@ -279,7 +280,7 @@ public static class EscSeqUtils
}
newConsoleKeyInfo = new ConsoleKeyInfo (
- '\0',
+ keyChar,
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
@@ -342,15 +343,26 @@ public static class EscSeqUtils
/// .
///
/// The value.
- /// The which may changes.
+ /// The which may change.
+ /// Normally is '\0' but on some cases may need other value.
/// The and probably the .
- public static ConsoleKey GetConsoleKey (char terminator, string? value, ref ConsoleModifiers mod)
+ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref ConsoleModifiers mod, ref char keyChar)
{
if (terminator == 'Z')
{
mod |= ConsoleModifiers.Shift;
}
+ if (terminator == 'l')
+ {
+ keyChar = '+';
+ }
+
+ if (terminator == 'm')
+ {
+ keyChar = '-';
+ }
+
return (terminator, value) switch
{
('A', _) => ConsoleKey.UpArrow,
@@ -376,6 +388,18 @@ public static class EscSeqUtils
('~', "21") => ConsoleKey.F10,
('~', "23") => ConsoleKey.F11,
('~', "24") => ConsoleKey.F12,
+ ('l', _) => ConsoleKey.Add,
+ ('m', _) => ConsoleKey.Subtract,
+ ('p', _) => ConsoleKey.Insert,
+ ('q', _) => ConsoleKey.End,
+ ('r', _) => ConsoleKey.DownArrow,
+ ('s', _) => ConsoleKey.PageDown,
+ ('t', _) => ConsoleKey.LeftArrow,
+ ('u', _) => ConsoleKey.Clear,
+ ('v', _) => ConsoleKey.RightArrow,
+ ('w', _) => ConsoleKey.Home,
+ ('x', _) => ConsoleKey.UpArrow,
+ ('y', _) => ConsoleKey.PageUp,
(_, _) => 0
};
}
diff --git a/Terminal.Gui/Drawing/Aligner.cs b/Terminal.Gui/Drawing/Aligner.cs
index 1c96a2ac0..47b4109f8 100644
--- a/Terminal.Gui/Drawing/Aligner.cs
+++ b/Terminal.Gui/Drawing/Aligner.cs
@@ -108,10 +108,11 @@ public class Aligner : INotifyPropertyChanged
spacesToGive = containerSize - totalItemsSize;
}
+ AlignmentModes mode = alignmentMode & ~AlignmentModes.AddSpaceBetweenItems; // copy to avoid modifying the original
switch (alignment)
{
case Alignment.Start:
- switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+ switch (mode)
{
case AlignmentModes.StartToEnd:
return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive);
@@ -129,7 +130,7 @@ public class Aligner : INotifyPropertyChanged
break;
case Alignment.End:
- switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+ switch (mode)
{
case AlignmentModes.StartToEnd:
return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
@@ -147,7 +148,8 @@ public class Aligner : INotifyPropertyChanged
break;
case Alignment.Center:
- switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+ mode &= ~AlignmentModes.IgnoreFirstOrLast;
+ switch (mode)
{
case AlignmentModes.StartToEnd:
return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
@@ -159,7 +161,8 @@ public class Aligner : INotifyPropertyChanged
break;
case Alignment.Fill:
- switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+ mode &= ~AlignmentModes.IgnoreFirstOrLast;
+ switch (mode)
{
case AlignmentModes.StartToEnd:
return Fill (in sizesCopy, containerSize, totalItemsSize);
@@ -260,7 +263,8 @@ public class Aligner : INotifyPropertyChanged
var currentPosition = 0;
if (totalItemsSize > containerSize)
{
- currentPosition = containerSize - totalItemsSize - spacesToGive;
+ // Don't allow negative positions
+ currentPosition = int.Max(0, containerSize - totalItemsSize - spacesToGive);
}
for (var i = 0; i < sizes.Length; i++)
diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs
index 5d3269226..5b6c23557 100644
--- a/Terminal.Gui/Input/CommandContext.cs
+++ b/Terminal.Gui/Input/CommandContext.cs
@@ -1,14 +1,17 @@
#nullable enable
namespace Terminal.Gui;
+
+#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
///
/// Provides context for a that is being invoked.
-///
///
///
/// To define a that is invoked with context,
-/// use
+/// use .
///
///
+#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
public record struct CommandContext
{
///
diff --git a/Terminal.Gui/Input/KeyBindingScope.cs b/Terminal.Gui/Input/KeyBindingScope.cs
index 9799fc831..3b6c53ebc 100644
--- a/Terminal.Gui/Input/KeyBindingScope.cs
+++ b/Terminal.Gui/Input/KeyBindingScope.cs
@@ -13,6 +13,9 @@ namespace Terminal.Gui;
[GenerateEnumExtensionMethods (FastHasFlags = true)]
public enum KeyBindingScope
{
+ /// The key binding is disabled.
+ Disabled = 0,
+
/// The key binding is scoped to just the view that has focus.
Focused = 1,
diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs
index 8ec38329e..a55169606 100644
--- a/Terminal.Gui/Input/KeyBindings.cs
+++ b/Terminal.Gui/Input/KeyBindings.cs
@@ -1,5 +1,7 @@
#nullable enable
+using System.Diagnostics;
+
namespace Terminal.Gui;
///
@@ -38,7 +40,7 @@ public class KeyBindings
else
{
Bindings.Add (key, binding);
- if (binding.Scope.FastHasFlags (KeyBindingScope.Application))
+ if (binding.Scope.HasFlag (KeyBindingScope.Application))
{
Application.AddKeyBinding (key, BoundView);
}
diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs
index 259eed499..e765414cb 100644
--- a/Terminal.Gui/View/Layout/Dim.cs
+++ b/Terminal.Gui/View/Layout/Dim.cs
@@ -110,14 +110,9 @@ public abstract class Dim
/// Specifies how will compute the dimension. The default is .
///
/// The minimum dimension the View's ContentSize will be constrained to.
- /// The maximum dimension the View's ContentSize will be fit to. NOT CURRENTLY SUPPORTED.
+ /// The maximum dimension the View's ContentSize will be fit to.
public static Dim? Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim? minimumContentDim = null, Dim? maximumContentDim = null)
{
- if (maximumContentDim is { })
- {
- Debug.WriteLine (@"WARNING: maximumContentDim is not fully implemented.");
- }
-
return new DimAuto ()
{
MinimumContentDim = minimumContentDim,
diff --git a/Terminal.Gui/View/Layout/DimAuto.cs b/Terminal.Gui/View/Layout/DimAuto.cs
index 0538c6f08..5d34a9729 100644
--- a/Terminal.Gui/View/Layout/DimAuto.cs
+++ b/Terminal.Gui/View/Layout/DimAuto.cs
@@ -1,4 +1,6 @@
#nullable enable
+using System.Drawing;
+
namespace Terminal.Gui;
///
@@ -81,7 +83,7 @@ public class DimAuto () : Dim
// TODO: This whole body of code is a WIP (for https://github.com/gui-cs/Terminal.Gui/pull/3451).
subviewsSize = 0;
- List includedSubviews = us.Subviews.ToList();//.Where (v => !v.ExcludeFromLayout).ToList ();
+ List includedSubviews = us.Subviews.ToList ();//.Where (v => !v.ExcludeFromLayout).ToList ();
List subviews;
#region Not Anchored and Are Not Dependent
@@ -100,14 +102,16 @@ public class DimAuto () : Dim
{
subviews = includedSubviews.Where (v => v.X is not PosAnchorEnd
&& v.X is not PosAlign
- // && v.X is not PosCenter
+ // && v.X is not PosCenter
+ && v.Width is not DimAuto
&& v.Width is not DimFill).ToList ();
}
else
{
subviews = includedSubviews.Where (v => v.Y is not PosAnchorEnd
&& v.Y is not PosAlign
- // && v.Y is not PosCenter
+ // && v.Y is not PosCenter
+ && v.Height is not DimAuto
&& v.Height is not DimFill).ToList ();
}
@@ -147,6 +151,88 @@ public class DimAuto () : Dim
subviewsSize += maxAnchorEnd;
#endregion Anchored
+ //#region Aligned
+
+ //// Now, handle subviews that are anchored to the end
+ //// [x] PosAnchorEnd
+ //int maxAlign = 0;
+ //if (dimension == Dimension.Width)
+ //{
+ // // Use Linq to get a list of distinct GroupIds from the subviews
+ // List groupIds = includedSubviews.Select (v => v.X is PosAlign posAlign ? posAlign.GroupId : -1).Distinct ().ToList ();
+
+ // foreach (var groupId in groupIds)
+ // {
+ // List dimensionsList = new ();
+
+ // // PERF: If this proves a perf issue, consider caching a ref to this list in each item
+ // List posAlignsInGroup = includedSubviews.Where (
+ // v =>
+ // {
+ // return dimension switch
+ // {
+ // Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
+ // Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
+ // _ => false
+ // };
+ // })
+ // .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
+ // .ToList ();
+
+ // if (posAlignsInGroup.Count == 0)
+ // {
+ // continue;
+ // }
+
+ // maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubviews, dimension);
+ // }
+ //}
+ //else
+ //{
+ // subviews = includedSubviews.Where (v => v.Y is PosAlign).ToList ();
+ //}
+
+ //subviewsSize = int.Max (subviewsSize, maxAlign);
+ //#endregion Aligned
+
+
+ #region Auto
+
+ if (dimension == Dimension.Width)
+ {
+ subviews = includedSubviews.Where (v => v.Width is DimAuto).ToList ();
+ }
+ else
+ {
+ subviews = includedSubviews.Where (v => v.Height is DimAuto).ToList ();
+ }
+
+ int maxAuto = 0;
+ for (var i = 0; i < subviews.Count; i++)
+ {
+ View v = subviews [i];
+
+ //if (dimension == Dimension.Width)
+ //{
+ // v.SetRelativeLayout (new Size (autoMax - subviewsSize, 0));
+ //}
+ //else
+ //{
+ // v.SetRelativeLayout (new Size (0, autoMax - subviewsSize));
+ //}
+ maxAuto = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height;
+
+ if (maxAuto > subviewsSize)
+ {
+ // BUGBUG: Should we break here? Or choose min/max?
+ subviewsSize = maxAuto;
+ }
+ }
+
+// subviewsSize += maxAuto;
+
+ #endregion Auto
+
//#region Center
//// Now, handle subviews that are Centered
//if (dimension == Dimension.Width)
@@ -174,15 +260,11 @@ public class DimAuto () : Dim
// [ ] DimPercent
if (dimension == Dimension.Width)
{
- subviews = includedSubviews.Where (v => v.Width is DimFill
- // || v.X is PosCenter
- ).ToList ();
+ subviews = includedSubviews.Where (v => v.Width is DimFill).ToList ();
}
else
{
- subviews = includedSubviews.Where (v => v.Height is DimFill
- //|| v.Y is PosCenter
- ).ToList ();
+ subviews = includedSubviews.Where (v => v.Height is DimFill).ToList ();
}
int maxFill = 0;
@@ -190,6 +272,10 @@ public class DimAuto () : Dim
{
View v = subviews [i];
+ if (autoMax == int.MaxValue)
+ {
+ autoMax = superviewContentSize;
+ }
if (dimension == Dimension.Width)
{
v.SetRelativeLayout (new Size (autoMax - subviewsSize, 0));
@@ -213,9 +299,11 @@ public class DimAuto () : Dim
// And, if min: is set, it wins if larger
max = int.Max (max, autoMin);
+ // And, if max: is set, it wins if smaller
+ max = int.Min (max, autoMax);
+
// Factor in adornments
Thickness thickness = us.GetAdornmentsThickness ();
-
max += dimension switch
{
Dimension.Width => thickness.Horizontal,
@@ -224,7 +312,7 @@ public class DimAuto () : Dim
_ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null)
};
- return int.Min (max, autoMax);
+ return max;
}
internal override bool ReferencesOtherViews ()
diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs
index a5cf52249..2213524ae 100644
--- a/Terminal.Gui/View/Layout/Pos.cs
+++ b/Terminal.Gui/View/Layout/Pos.cs
@@ -336,6 +336,30 @@ public abstract class Pos
///
internal virtual bool ReferencesOtherViews () { return false; }
+ ///
+ /// Indicates whether the specified type is in the hierarchy of this Pos object.
+ ///
+ ///
+ ///
+ ///
+ public bool Has (Type type, out Pos pos)
+ {
+ pos = this;
+ if (type == GetType ())
+ {
+ return true;
+ }
+
+ // If we are a PosCombine, we have to check the left and right
+ // to see if they are of the type we are looking for.
+ if (this is PosCombine { } combine && (combine.Left.Has (type, out pos) || combine.Right.Has (type, out pos)))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
#endregion virtual methods
#region operators
diff --git a/Terminal.Gui/View/Layout/PosAlign.cs b/Terminal.Gui/View/Layout/PosAlign.cs
index 31bb81a26..79e22d410 100644
--- a/Terminal.Gui/View/Layout/PosAlign.cs
+++ b/Terminal.Gui/View/Layout/PosAlign.cs
@@ -29,7 +29,7 @@ public class PosAlign : Pos
///
/// The cached location. Used to store the calculated location to minimize recalculating it.
///
- private int? _cachedLocation;
+ public int? _cachedLocation;
///
/// Gets the identifier of a set of views that should be aligned together. When only a single
@@ -70,59 +70,67 @@ public class PosAlign : Pos
List dimensionsList = new ();
// PERF: If this proves a perf issue, consider caching a ref to this list in each item
- List viewsInGroup = views.Where (
- v =>
- {
- return dimension switch
- {
- Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
- Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
- _ => false
- };
- })
- .ToList ();
+ List posAligns = views.Select (
+ v =>
+ {
+ switch (dimension)
+ {
+ case Dimension.Width when v.X.Has (typeof (PosAlign), out var pos):
- if (viewsInGroup.Count == 0)
- {
- return;
- }
+ if (pos is PosAlign posAlignX && posAlignX.GroupId == groupId)
+ {
+ return posAlignX;
+ }
+
+ break;
+ case Dimension.Height when v.Y.Has (typeof (PosAlign), out var pos):
+ if (pos is PosAlign posAlignY && posAlignY.GroupId == groupId)
+ {
+ return posAlignY;
+ }
+
+ break;
+ }
+
+ return null;
+ })
+ .ToList ();
// PERF: We iterate over viewsInGroup multiple times here.
Aligner? firstInGroup = null;
// Update the dimensionList with the sizes of the views
- for (var index = 0; index < viewsInGroup.Count; index++)
+ for (var index = 0; index < posAligns.Count; index++)
{
- View view = viewsInGroup [index];
- PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
-
- if (posAlign is { })
+ if (posAligns [index] is { })
{
- if (index == 0)
+ if (firstInGroup is null)
{
- firstInGroup = posAlign.Aligner;
+ firstInGroup = posAligns [index]!.Aligner;
}
- dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+ dimensionsList.Add (dimension == Dimension.Width ? views [index].Frame.Width : views [index].Frame.Height);
}
}
+ if (firstInGroup is null)
+ {
+ return;
+ }
+
// Update the first item in the group with the new container size.
- firstInGroup!.ContainerSize = size;
+ firstInGroup.ContainerSize = size;
// Align
int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
// Update the cached location for each item
- for (var index = 0; index < viewsInGroup.Count; index++)
+ for (int posIndex = 0, locIndex = 0; posIndex < posAligns.Count; posIndex++)
{
- View view = viewsInGroup [index];
- PosAlign? align = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
-
- if (align is { })
+ if (posAligns [posIndex] is { })
{
- align._cachedLocation = locations [index];
+ posAligns [posIndex]!._cachedLocation = locations [locIndex++];
}
}
}
@@ -168,7 +176,15 @@ public class PosAlign : Pos
return 0;
}
- internal int CalculateMinDimension (int groupId, IList views, Dimension dimension)
+ // TODO: PosAlign.CalculateMinDimension is a hack. Need to figure out a better way of doing this.
+ ///
+ /// Returns the minimum size a group of views with the same can be.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static int CalculateMinDimension (int groupId, IList views, Dimension dimension)
{
List dimensionsList = new ();
@@ -177,11 +193,11 @@ public class PosAlign : Pos
v =>
{
return dimension switch
- {
- Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
- Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
- _ => false
- };
+ {
+ Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
+ Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
+ _ => false
+ };
})
.ToList ();
@@ -192,8 +208,6 @@ public class PosAlign : Pos
// PERF: We iterate over viewsInGroup multiple times here.
- Aligner? firstInGroup = null;
-
// Update the dimensionList with the sizes of the views
for (var index = 0; index < viewsInGroup.Count; index++)
{
@@ -203,20 +217,11 @@ public class PosAlign : Pos
if (posAlign is { })
{
- if (index == 0)
- {
- firstInGroup = posAlign.Aligner;
- }
-
dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
}
}
// Align
- var aligner = firstInGroup;
- aligner.ContainerSize = dimensionsList.Sum();
- int [] locations = aligner.Align (dimensionsList.ToArray ());
-
- return locations.Sum ();
+ return dimensionsList.Sum ();
}
}
diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs
index 441b0a3ac..654a4d915 100644
--- a/Terminal.Gui/View/View.cs
+++ b/Terminal.Gui/View/View.cs
@@ -230,6 +230,7 @@ public partial class View : Responder, ISupportInitializeNotification
}
Initialized?.Invoke (this, EventArgs.Empty);
+
}
#endregion Constructors and Initialization
diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/ViewAdornments.cs
index 0ebf5499f..89b8ff0e7 100644
--- a/Terminal.Gui/View/ViewAdornments.cs
+++ b/Terminal.Gui/View/ViewAdornments.cs
@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using System.ComponentModel;
+
+namespace Terminal.Gui;
public partial class View
{
@@ -95,29 +97,77 @@ public partial class View
get => Border?.LineStyle ?? LineStyle.Single;
set
{
- if (Border is null)
- {
- return;
- }
+ StateEventArgs e = new (Border?.LineStyle ?? LineStyle.None, value);
+ OnBorderStyleChanging (e);
- if (value != LineStyle.None)
- {
- if (Border.Thickness == Thickness.Empty)
- {
- Border.Thickness = new (1);
- }
- }
- else
- {
- Border.Thickness = new (0);
- }
-
- Border.LineStyle = value;
- LayoutAdornments ();
- SetNeedsLayout ();
}
}
+ ///
+ /// Called when the is changing. Invokes , which allows the event to be cancelled.
+ ///
+ ///
+ /// Override to prevent the from changing.
+ ///
+ ///
+ protected void OnBorderStyleChanging (StateEventArgs e)
+ {
+ if (Border is null)
+ {
+ return;
+ }
+
+ BorderStyleChanging?.Invoke (this, e);
+ if (e.Cancel)
+ {
+ return;
+ }
+
+ SetBorderStyle (e.NewValue);
+ LayoutAdornments ();
+ SetNeedsLayout ();
+
+ return;
+ }
+
+ ///
+ /// Sets the of the view to the specified value.
+ ///
+ ///
+ ///
+ /// is a helper for manipulating the view's . Setting this property to any value other
+ /// than is equivalent to setting 's
+ /// to `1` and to the value.
+ ///
+ ///
+ /// Setting this property to is equivalent to setting 's
+ /// to `0` and to .
+ ///
+ /// For more advanced customization of the view's border, manipulate see directly.
+ ///
+ ///
+ public virtual void SetBorderStyle (LineStyle value)
+ {
+ if (value != LineStyle.None)
+ {
+ if (Border.Thickness == Thickness.Empty)
+ {
+ Border.Thickness = new (1);
+ }
+ }
+ else
+ {
+ Border.Thickness = new (0);
+ }
+
+ Border.LineStyle = value;
+ }
+
+ ///
+ /// Fired when the is changing. Allows the event to be cancelled.
+ ///
+ public event EventHandler> BorderStyleChanging;
+
///
/// The inside of the view that offsets the
/// from the .
diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs
index 74beffd3f..604b7d51a 100644
--- a/Terminal.Gui/View/ViewDrawing.cs
+++ b/Terminal.Gui/View/ViewDrawing.cs
@@ -640,17 +640,17 @@ public partial class View
{
SubViewNeedsDisplay = true;
+ if (this is Adornment adornment)
+ {
+ adornment.Parent?.SetSubViewNeedsDisplay ();
+ }
+
if (SuperView is { SubViewNeedsDisplay: false })
{
SuperView.SetSubViewNeedsDisplay ();
return;
}
-
- if (this is Adornment adornment)
- {
- adornment.Parent?.SetSubViewNeedsDisplay ();
- }
}
/// Clears and .
diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs
index 7863c6799..d1f04c654 100644
--- a/Terminal.Gui/View/ViewKeyboard.cs
+++ b/Terminal.Gui/View/ViewKeyboard.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using System.Diagnostics;
namespace Terminal.Gui;
@@ -414,7 +415,7 @@ public partial class View
return true;
}
- bool? handled = OnInvokingKeyBindings (keyEvent);
+ bool? handled = OnInvokingKeyBindings (keyEvent, KeyBindingScope.HotKey | KeyBindingScope.Focused);
if (handled is { } && (bool)handled)
{
@@ -636,10 +637,10 @@ public partial class View
/// if the key press was not handled. if the keypress was handled
/// and no other view should see it.
///
- public virtual bool? OnInvokingKeyBindings (Key keyEvent)
+ public virtual bool? OnInvokingKeyBindings (Key keyEvent, KeyBindingScope scope)
{
- // fire event only if there's an app or hotkey binding for the key
- if (KeyBindings.TryGet (keyEvent, KeyBindingScope.Application | KeyBindingScope.HotKey, out KeyBinding _))
+ // fire event only if there's an hotkey binding for the key
+ if (KeyBindings.TryGet (keyEvent, scope, out KeyBinding kb))
{
InvokingKeyBindings?.Invoke (this, keyEvent);
if (keyEvent.Handled)
@@ -655,7 +656,7 @@ public partial class View
// `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`)..
// * If key bindings were found, and any handled the key (at least one `Command` returned `true`),
// `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).
- bool? handled = InvokeKeyBindings (keyEvent);
+ bool? handled = InvokeKeyBindings (keyEvent, scope);
if (handled is { } && (bool)handled)
{
@@ -664,22 +665,22 @@ public partial class View
return true;
}
- if (Margin is { } && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
+ if (Margin is { } && ProcessAdornmentKeyBindings (Margin, keyEvent, scope, ref handled))
{
return true;
}
- if (Padding is { } && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
+ if (Padding is { } && ProcessAdornmentKeyBindings (Padding, keyEvent, scope, ref handled))
{
return true;
}
- if (Border is { } && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
+ if (Border is { } && ProcessAdornmentKeyBindings (Border, keyEvent, scope, ref handled))
{
return true;
}
- if (ProcessSubViewKeyBindings (keyEvent, ref handled))
+ if (ProcessSubViewKeyBindings (keyEvent, scope, ref handled))
{
return true;
}
@@ -687,11 +688,11 @@ public partial class View
return handled;
}
- private bool ProcessAdornmentKeyBindings (Adornment adornment, Key keyEvent, ref bool? handled)
+ private bool ProcessAdornmentKeyBindings (Adornment adornment, Key keyEvent, KeyBindingScope scope, ref bool? handled)
{
foreach (View subview in adornment?.Subviews)
{
- handled = subview.OnInvokingKeyBindings (keyEvent);
+ handled = subview.OnInvokingKeyBindings (keyEvent, scope);
if (handled is { } && (bool)handled)
{
@@ -702,26 +703,71 @@ public partial class View
return false;
}
- private bool ProcessSubViewKeyBindings (Key keyEvent, ref bool? handled)
+ private bool ProcessSubViewKeyBindings (Key keyEvent, KeyBindingScope scope, ref bool? handled, bool invoke = true)
{
// Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey.
foreach (View subview in Subviews)
{
- if (subview.KeyBindings.TryGet (keyEvent, KeyBindingScope.HotKey, out KeyBinding binding))
+ if (subview.KeyBindings.TryGet (keyEvent, scope, out KeyBinding binding))
{
- //keyEvent.Scope = KeyBindingScope.HotKey;
- handled = subview.OnInvokingKeyBindings (keyEvent);
+ if (binding.Scope == KeyBindingScope.Focused && !subview.HasFocus)
+ {
+ continue;
+ }
+
+ if (!invoke)
+ {
+ return true;
+ }
+
+ handled = subview.OnInvokingKeyBindings (keyEvent, scope);
if (handled is { } && (bool)handled)
{
return true;
}
}
+
+ bool recurse = subview.ProcessSubViewKeyBindings (keyEvent, scope, ref handled, invoke);
+ if (recurse || (handled is { } && (bool)handled))
+ {
+ return true;
+ }
}
return false;
}
+ // TODO: This is a "prototype" debug check. It may be too annyoing vs. useful.
+ // TODO: A better approach would be have Application hold a list of bound Hotkeys, similar to
+ // TODO: how Application holds a list of Application Scoped key bindings and then check that list.
+ ///
+ /// Returns true if Key is bound in this view heirarchy. For debugging
+ ///
+ ///
+ ///
+ public bool IsHotKeyKeyBound (Key key, out View boundView)
+ {
+ // recurse through the subviews to find the views that has the key bound
+ boundView = null;
+
+ foreach (View subview in Subviews)
+ {
+ if (subview.KeyBindings.TryGet (key, KeyBindingScope.HotKey, out _))
+ {
+ boundView = subview;
+ return true;
+ }
+
+ if (subview.IsHotKeyKeyBound (key, out boundView))
+ {
+ return true;
+ }
+
+ }
+ return false;
+ }
+
///
/// Invoked when a key is pressed that may be mapped to a key binding. Set to true to
/// stop the key from being processed by other views.
@@ -738,15 +784,36 @@ public partial class View
/// commands were invoked and at least one handled the command. if commands were invoked and at
/// none handled the command.
///
- protected bool? InvokeKeyBindings (Key key)
+ protected bool? InvokeKeyBindings (Key key, KeyBindingScope scope)
{
bool? toReturn = null;
- if (!KeyBindings.TryGet (key, out KeyBinding binding))
+ if (!KeyBindings.TryGet (key, scope, out KeyBinding binding))
{
return null;
}
+#if DEBUG
+
+ // TODO: Determine if App scope bindings should be fired first or last (currently last).
+ if (Application.TryGetKeyBindings (key, out List views))
+ {
+ var boundView = views [0];
+ var commandBinding = boundView.KeyBindings.Get (key);
+ Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command.{commandBinding.Commands [0]}: {boundView}.");
+ }
+
+ // TODO: This is a "prototype" debug check. It may be too annyoing vs. useful.
+ // Scour the bindings up our View heirarchy
+ // to ensure that the key is not already bound to a different set of commands.
+ if (SuperView?.IsHotKeyKeyBound (key, out View previouslyBoundView) ?? false)
+ {
+ Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}.");
+ }
+
+#endif
+
+
foreach (Command command in binding.Commands)
{
if (!CommandImplementations.ContainsKey (command))
@@ -777,10 +844,11 @@ public partial class View
///
///
/// The key that caused the commands to be invoked, if any.
+ ///
///
/// if no command was found.
- /// if the command was invoked and it handled the command.
- /// if the command was invoked and it did not handle the command.
+ /// if the command was invoked the command was handled.
+ /// if the command was invoked and the command was not handled.
///
public bool? InvokeCommands (Command [] commands, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null)
{
@@ -871,7 +939,7 @@ public partial class View
/// The function.
protected void AddCommand (Command command, Func f)
{
- CommandImplementations [command] = ctx => f (); ;
+ CommandImplementations [command] = ctx => f ();
}
/// Returns all commands that are supported by this .
diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs
index 9fccc0635..940861be3 100644
--- a/Terminal.Gui/View/ViewSubViews.cs
+++ b/Terminal.Gui/View/ViewSubViews.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics;
+
namespace Terminal.Gui;
public partial class View
@@ -40,11 +42,13 @@ public partial class View
/// the lifecycle of the subviews to be transferred to this View.
///
///
- public virtual void Add (View view)
+ /// The view to add.
+ /// The view that was added.
+ public virtual View Add (View view)
{
if (view is null)
{
- return;
+ return view;
}
if (_subviews is null)
@@ -72,6 +76,7 @@ public partial class View
SuperView._addingView = false;
}
+ // QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying.
CanFocus = true;
view._tabIndex = _tabIndexes.IndexOf (view);
_addingView = false;
@@ -94,6 +99,8 @@ public partial class View
CheckDimAuto ();
SetNeedsLayout ();
SetNeedsDisplay ();
+
+ return view;
}
/// Adds the specified views (children) to the view.
@@ -205,11 +212,11 @@ public partial class View
/// lifecycle to be transferred to the caller; the caller muse call .
///
///
- public virtual void Remove (View view)
+ public virtual View Remove (View view)
{
if (view is null || _subviews is null)
{
- return;
+ return view;
}
Rectangle touched = view.Frame;
@@ -234,6 +241,8 @@ public partial class View
{
Focused = null;
}
+
+ return view;
}
///
@@ -438,7 +447,7 @@ public partial class View
SetHasFocus (false, this);
SuperView?.EnsureFocus ();
- if (SuperView is { } && SuperView.Focused is null)
+ if (SuperView is { Focused: null })
{
SuperView.FocusNext ();
@@ -477,6 +486,11 @@ public partial class View
}
}
}
+
+ if (this is Toplevel && Application.Current.Focused != this)
+ {
+ Application.BringOverlappedTopToFront ();
+ }
}
OnCanFocusChanged ();
diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs
new file mode 100644
index 000000000..07447fba8
--- /dev/null
+++ b/Terminal.Gui/Views/Bar.cs
@@ -0,0 +1,229 @@
+namespace Terminal.Gui;
+
+///
+/// Provides a horizontally or vertically oriented container for s to be used as a menu, toolbar, or status
+/// bar.
+///
+///
+///
+/// Any can be added to a . However, the is designed to work with
+/// objects. The class provides a way to display a command, help, and key and
+/// align them in a specific order.
+///
+///
+public class Bar : View
+{
+ ///
+ public Bar () : this ([]) { }
+
+ ///
+ public Bar (IEnumerable shortcuts)
+ {
+ CanFocus = true;
+
+ Width = Dim.Auto ();
+ Height = Dim.Auto ();
+
+ Initialized += Bar_Initialized;
+
+ if (shortcuts is null)
+ {
+ return;
+ }
+
+ foreach (Shortcut shortcut in shortcuts)
+ {
+ Add (shortcut);
+ }
+ }
+
+ private void Bar_Initialized (object sender, EventArgs e) { ColorScheme = Colors.ColorSchemes ["Menu"]; }
+
+ ///
+ public override void SetBorderStyle (LineStyle value)
+ {
+ // The default changes the thickness. We don't want that. We just set the style.
+ Border.LineStyle = value;
+ }
+
+ private Orientation _orientation = Orientation.Horizontal;
+
+ ///
+ /// Gets or sets the for this . The default is
+ /// .
+ ///
+ ///
+ ///
+ /// Horizontal orientation arranges the command, help, and key parts of each s from right to left
+ /// Vertical orientation arranges the command, help, and key parts of each s from left to right.
+ ///
+ ///
+ public Orientation Orientation
+ {
+ get => _orientation;
+ set
+ {
+ _orientation = value;
+ SetNeedsLayout ();
+ }
+ }
+
+ private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd;
+
+ ///
+ /// Gets or sets the for this . The default is
+ /// .
+ ///
+ public AlignmentModes AlignmentModes
+ {
+ get => _alignmentModes;
+ set
+ {
+ _alignmentModes = value;
+ SetNeedsLayout ();
+ }
+ }
+
+ // TODO: Move this to View
+ /// Inserts a in the specified index of .
+ /// The zero-based index at which item should be inserted.
+ /// The item to insert.
+ public void AddShortcutAt (int index, Shortcut item)
+ {
+ List savedSubViewList = Subviews.ToList ();
+ int count = savedSubViewList.Count;
+ RemoveAll ();
+
+ for (var i = 0; i <= count; i++)
+ {
+ if (i == index)
+ {
+ Add (item);
+ }
+
+ if (i < count)
+ {
+ Add (savedSubViewList [i]);
+ }
+ }
+
+ SetNeedsDisplay ();
+ }
+
+ // TODO: Move this to View
+
+ /// Removes a at specified index of .
+ /// The zero-based index of the item to remove.
+ /// The removed.
+ public Shortcut RemoveShortcut (int index)
+ {
+ View toRemove = null;
+
+ for (var i = 0; i < Subviews.Count; i++)
+ {
+ if (i == index)
+ {
+ toRemove = Subviews [i];
+ }
+ }
+
+ if (toRemove is { })
+ {
+ Remove (toRemove);
+ SetNeedsDisplay ();
+ }
+
+ return toRemove as Shortcut;
+ }
+
+ ///
+ internal override void OnLayoutStarted (LayoutEventArgs args)
+ {
+ base.OnLayoutStarted (args);
+
+ View prevBarItem = null;
+
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ for (var index = 0; index < Subviews.Count; index++)
+ {
+ View barItem = Subviews [index];
+
+ barItem.ColorScheme = ColorScheme;
+ barItem.X = Pos.Align (Alignment.Start, AlignmentModes);
+ barItem.Y = 0; //Pos.Center ();
+
+ // HACK: This should not be needed
+ barItem.SetRelativeLayout (GetContentSize ());
+ }
+
+ break;
+
+ case Orientation.Vertical:
+ // Set the overall size of the Bar and arrange the views vertically
+
+ var minKeyWidth = 0;
+
+ List shortcuts = Subviews.Where (s => s is Shortcut && s.Visible).Cast ().ToList ();
+ foreach (Shortcut shortcut in shortcuts)
+ {
+ // Let AutoSize do its thing to get the minimum width of each CommandView and HelpView
+ //shortcut.CommandView.SetRelativeLayout (new Size (int.MaxValue, int.MaxValue));
+ minKeyWidth = int.Max (minKeyWidth, shortcut.KeyView.Text.GetColumns ());
+ }
+
+ var maxBarItemWidth = 0;
+ var totalHeight = 0;
+
+ for (var index = 0; index < Subviews.Count; index++)
+ {
+ View barItem = Subviews [index];
+
+ barItem.ColorScheme = ColorScheme;
+
+ if (!barItem.Visible)
+ {
+ continue;
+ }
+
+ if (barItem is Shortcut scBarItem)
+ {
+ scBarItem.MinimumKeyTextSize = minKeyWidth;
+ // HACK: This should not be needed
+ scBarItem.SetRelativeLayout (GetContentSize ());
+ maxBarItemWidth = Math.Max (maxBarItemWidth, scBarItem.Frame.Width);
+ }
+
+ if (prevBarItem == null)
+ {
+ barItem.Y = 0;
+ }
+ else
+ {
+ // Align the view to the bottom of the previous view
+ barItem.Y = Pos.Bottom (prevBarItem);
+ }
+
+ prevBarItem = barItem;
+
+ barItem.X = 0;
+ totalHeight += barItem.Frame.Height;
+ }
+
+
+ foreach (View barItem in Subviews)
+ {
+ barItem.Width = maxBarItemWidth;
+
+ if (barItem is Line line)
+ {
+ }
+ }
+
+ Height = Dim.Auto (DimAutoStyle.Content, totalHeight);
+
+ break;
+ }
+ }
+}
diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs
index addf7a1a4..c033cb4f0 100644
--- a/Terminal.Gui/Views/ColorPicker.cs
+++ b/Terminal.Gui/Views/ColorPicker.cs
@@ -48,7 +48,7 @@ public class ColorPicker : View
private void ColorPicker_MouseClick (object sender, MouseEventEventArgs me)
{
- if (CanFocus)
+ // if (CanFocus)
{
Cursor = new Point (me.MouseEvent.Position.X / _boxWidth, me.MouseEvent.Position.Y / _boxHeight);
SetFocus ();
diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs
index 36af3853e..a5d3ed4a2 100644
--- a/Terminal.Gui/Views/ComboBox.cs
+++ b/Terminal.Gui/Views/ComboBox.cs
@@ -187,7 +187,7 @@ public class ComboBox : View
{
SelectedItem = -1;
_search.Text = string.Empty;
- Search_Changed (this, new StateEventArgs (string.Empty, _search.Text));
+ Search_Changed (this, new StateEventArgs (string.Empty, _search.Text));
SetNeedsDisplay ();
}
}
@@ -645,7 +645,9 @@ public class ComboBox : View
private void ResetSearchSet (bool noCopy = false)
{
+ _listview.SuspendCollectionChangedEvent ();
_searchSet.Clear ();
+ _listview.ResumeSuspendCollectionChangedEvent ();
if (_autoHide || noCopy)
{
@@ -682,6 +684,8 @@ public class ComboBox : View
if (!string.IsNullOrEmpty (_search.Text))
{
+ _listview.SuspendCollectionChangedEvent ();
+
foreach (object item in _source.ToList ())
{
// Iterate to preserver object type and force deep copy
@@ -694,6 +698,8 @@ public class ComboBox : View
_searchSet.Add (item);
}
}
+
+ _listview.ResumeSuspendCollectionChangedEvent ();
}
}
@@ -738,11 +744,16 @@ public class ComboBox : View
return;
}
+ // PERF: At the request of @dodexahedron in the comment https://github.com/gui-cs/Terminal.Gui/pull/3552#discussion_r1648112410.
+ _listview.SuspendCollectionChangedEvent ();
+
// force deep copy
foreach (object item in Source.ToList ())
{
_searchSet.Add (item);
}
+
+ _listview.ResumeSuspendCollectionChangedEvent ();
}
private void SetSearchText (string value) { _search.Text = _text = value; }
@@ -765,8 +776,11 @@ public class ComboBox : View
/// Consider making public
private void ShowList ()
{
+ _listview.SuspendCollectionChangedEvent ();
_listview.SetSource (_searchSet);
- _listview.Clear ();
+ _listview.ResumeSuspendCollectionChangedEvent ();
+
+ _listview.Clear ();
_listview.Height = CalculatetHeight ();
SuperView?.BringSubviewToFront (this);
}
diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs
index 96a55f229..2ab97a98e 100644
--- a/Terminal.Gui/Views/Dialog.cs
+++ b/Terminal.Gui/Views/Dialog.cs
@@ -9,7 +9,7 @@ namespace Terminal.Gui;
///
///
/// To run the modally, create the , and pass it to
-/// . This will execute the dialog until
+/// . This will execute the dialog until
/// it terminates via the
/// [ESC] or [CTRL-Q] key, or when one of the views or buttons added to the dialog calls
/// .
diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs
index a55386413..40e4e9030 100644
--- a/Terminal.Gui/Views/HexView.cs
+++ b/Terminal.Gui/Views/HexView.cs
@@ -249,8 +249,6 @@ public class HexView : View
///
protected internal override bool OnMouseEvent (MouseEvent me)
{
- // BUGBUG: Test this with a border! Assumes Frame == Viewport!
-
if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
&& !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
&& !me.Flags.HasFlag (MouseFlags.WheeledDown)
@@ -343,20 +341,17 @@ public class HexView : View
Driver.SetAttribute (current);
Move (0, 0);
- // BUGBUG: Viewport!!!!
- Rectangle frame = Frame;
-
int nblocks = bytesPerLine / bsize;
- var data = new byte [nblocks * bsize * frame.Height];
+ var data = new byte [nblocks * bsize * viewport.Height];
Source.Position = displayStart;
int n = source.Read (data, 0, data.Length);
Attribute activeColor = ColorScheme.HotNormal;
Attribute trackingColor = ColorScheme.HotFocus;
- for (var line = 0; line < frame.Height; line++)
+ for (var line = 0; line < viewport.Height; line++)
{
- Rectangle lineRect = new (0, line, frame.Width, 1);
+ Rectangle lineRect = new (0, line, viewport.Width, 1);
if (!Viewport.Contains (lineRect))
{
@@ -597,16 +592,15 @@ public class HexView : View
private bool MoveDown (int bytes)
{
- // BUGBUG: Viewport!
RedisplayLine (position);
if (position + bytes < source.Length)
{
position += bytes;
}
- else if ((bytes == bytesPerLine * Frame.Height && source.Length >= DisplayStart + bytesPerLine * Frame.Height)
- || (bytes <= bytesPerLine * Frame.Height - bytesPerLine
- && source.Length <= DisplayStart + bytesPerLine * Frame.Height))
+ else if ((bytes == bytesPerLine * Viewport.Height && source.Length >= DisplayStart + bytesPerLine * Viewport.Height)
+ || (bytes <= bytesPerLine * Viewport.Height - bytesPerLine
+ && source.Length <= DisplayStart + bytesPerLine * Viewport.Height))
{
long p = position;
@@ -618,7 +612,7 @@ public class HexView : View
position = p;
}
- if (position >= DisplayStart + bytesPerLine * Frame.Height)
+ if (position >= DisplayStart + bytesPerLine * Viewport.Height)
{
SetDisplayStart (DisplayStart + bytes);
SetNeedsDisplay ();
@@ -635,8 +629,7 @@ public class HexView : View
{
position = source.Length;
- // BUGBUG: Viewport!
- if (position >= DisplayStart + bytesPerLine * Frame.Height)
+ if (position >= DisplayStart + bytesPerLine * Viewport.Height)
{
SetDisplayStart (position);
SetNeedsDisplay ();
@@ -722,8 +715,7 @@ public class HexView : View
position++;
}
- // BUGBUG: Viewport!
- if (position >= DisplayStart + bytesPerLine * Frame.Height)
+ if (position >= DisplayStart + bytesPerLine * Viewport.Height)
{
SetDisplayStart (DisplayStart + bytesPerLine);
SetNeedsDisplay ();
@@ -771,8 +763,7 @@ public class HexView : View
var delta = (int)(pos - DisplayStart);
int line = delta / bytesPerLine;
- // BUGBUG: Viewport!
- SetNeedsDisplay (new (0, line, Frame.Width, 1));
+ SetNeedsDisplay (new (0, line, Viewport.Width, 1));
}
private bool ToggleSide ()
diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs
index 8ef294061..95a8287c8 100644
--- a/Terminal.Gui/Views/Line.cs
+++ b/Terminal.Gui/Views/Line.cs
@@ -8,26 +8,81 @@ public class Line : View
{
BorderStyle = LineStyle.Single;
Border.Thickness = new Thickness (0);
+ SuperViewRendersLineCanvas = true;
}
+ public Dim Length { get; set; } = Dim.Fill ();
+
+ private Orientation _orientation;
+
///
/// The direction of the line. If you change this you will need to manually update the Width/Height of the
/// control to cover a relevant area based on the new direction.
///
- public Orientation Orientation { get; set; }
+ public Orientation Orientation
+ {
+ get => _orientation;
+ set
+ {
+ _orientation = value;
+
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ Height = 1;
+ // Width = Length;
+ //Border.Thickness = new Thickness (1, 0, 1, 0);
+
+ break;
+ case Orientation.Vertical:
+ Height = Length;
+ Width = 1;
+
+ break;
+
+ }
+ }
+ }
+
+ ///
+ public override void SetBorderStyle (LineStyle value)
+ {
+ // The default changes the thickness. We don't want that. We just set the style.
+ Border.LineStyle = value;
+ }
///
public override void OnDrawContent (Rectangle viewport)
{
LineCanvas lc = LineCanvas;
+ if (SuperViewRendersLineCanvas)
+ {
+ lc = SuperView.LineCanvas;
+ }
+
if (SuperView is Adornment adornment)
{
lc = adornment.Parent.LineCanvas;
}
+
+ Point pos = ViewportToScreen (viewport).Location;
+ int length = Orientation == Orientation.Horizontal ? Frame.Width : Frame.Height;
+
+ if (SuperViewRendersLineCanvas && Orientation == Orientation.Horizontal)
+ {
+ pos.Offset (-SuperView.Border.Thickness.Left, 0);
+ length += SuperView.Border.Thickness.Horizontal;
+ }
+
+ if (SuperViewRendersLineCanvas && Orientation == Orientation.Vertical)
+ {
+ pos.Offset (0, -SuperView.Border.Thickness.Top);
+ length += SuperView.Border.Thickness.Vertical;
+ }
lc.AddLine (
- ViewportToScreen (viewport).Location,
- Orientation == Orientation.Horizontal ? Frame.Width : Frame.Height,
+ pos,
+ length,
Orientation,
BorderStyle
);
diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs
index 296cb2717..5701c3a8b 100644
--- a/Terminal.Gui/Views/ListView.cs
+++ b/Terminal.Gui/Views/ListView.cs
@@ -19,6 +19,12 @@ public interface IListDataSource: IDisposable
/// Returns the maximum length of elements to display
int Length { get; }
+ ///
+ /// Allow suspending the event from being invoked,
+ /// if , otherwise is .
+ ///
+ bool SuspendCollectionChangedEvent { get; set; }
+
/// Should return whether the specified item is currently marked.
/// , if marked, otherwise.
/// Item index.
@@ -893,6 +899,28 @@ public class ListView : View
base.Dispose (disposing);
}
+
+ ///
+ /// Allow suspending the event from being invoked,
+ ///
+ public void SuspendCollectionChangedEvent ()
+ {
+ if (Source is { })
+ {
+ Source.SuspendCollectionChangedEvent = true;
+ }
+ }
+
+ ///
+ /// Allow resume the event from being invoked,
+ ///
+ public void ResumeSuspendCollectionChangedEvent ()
+ {
+ if (Source is { })
+ {
+ Source.SuspendCollectionChangedEvent = false;
+ }
+ }
}
///
@@ -920,8 +948,11 @@ public class ListWrapper : IListDataSource, IDisposable
private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
{
- CheckAndResizeMarksIfRequired ();
- CollectionChanged?.Invoke (sender, e);
+ if (!SuspendCollectionChangedEvent)
+ {
+ CheckAndResizeMarksIfRequired ();
+ CollectionChanged?.Invoke (sender, e);
+ }
}
///
@@ -933,7 +964,24 @@ public class ListWrapper : IListDataSource, IDisposable
///
public int Length { get; private set; }
- void CheckAndResizeMarksIfRequired ()
+ private bool _suspendCollectionChangedEvent;
+
+ ///
+ public bool SuspendCollectionChangedEvent
+ {
+ get => _suspendCollectionChangedEvent;
+ set
+ {
+ _suspendCollectionChangedEvent = value;
+
+ if (!_suspendCollectionChangedEvent)
+ {
+ CheckAndResizeMarksIfRequired ();
+ }
+ }
+ }
+
+ private void CheckAndResizeMarksIfRequired ()
{
if (_source != null && _count != _source.Count)
{
diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs
index 89d8548e9..74bf85828 100644
--- a/Terminal.Gui/Views/Menu/Menu.cs
+++ b/Terminal.Gui/Views/Menu/Menu.cs
@@ -273,9 +273,9 @@ internal sealed class Menu : View
}
///
- public override bool? OnInvokingKeyBindings (Key keyEvent)
+ public override bool? OnInvokingKeyBindings (Key keyEvent, KeyBindingScope scope)
{
- bool? handled = base.OnInvokingKeyBindings (keyEvent);
+ bool? handled = base.OnInvokingKeyBindings (keyEvent, scope);
if (handled is { } && (bool)handled)
{
@@ -284,7 +284,7 @@ internal sealed class Menu : View
// TODO: Determine if there's a cleaner way to handle this
// This supports the case where the menu bar is a context menu
- return _host.OnInvokingKeyBindings (keyEvent);
+ return _host.OnInvokingKeyBindings (keyEvent, scope);
}
private void Current_TerminalResized (object sender, SizeChangedEventArgs e)
diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs
index 08b045f81..9111f6bc2 100644
--- a/Terminal.Gui/Views/Menu/MenuBar.cs
+++ b/Terminal.Gui/Views/Menu/MenuBar.cs
@@ -136,6 +136,8 @@ public class MenuBar : View
// TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key?
KeyBindings.Add (Key.Space.WithCtrl, keyBinding);
+ // This is needed for macOS because Key.Space.WithCtrl doesn't work
+ KeyBindings.Add (Key.Space.WithAlt, keyBinding);
// TODO: Figure out how to make Alt work (on Windows)
//KeyBindings.Add (Key.WithAlt, keyBinding);
diff --git a/Terminal.Gui/Views/MenuBarv2.cs b/Terminal.Gui/Views/MenuBarv2.cs
new file mode 100644
index 000000000..12278a24c
--- /dev/null
+++ b/Terminal.Gui/Views/MenuBarv2.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Reflection;
+
+namespace Terminal.Gui;
+
+///
+/// A menu bar is a that snaps to the top of a displaying set of
+/// s.
+///
+public class MenuBarv2 : Bar
+{
+ ///
+ public MenuBarv2 () : this ([]) { }
+
+ ///
+ public MenuBarv2 (IEnumerable shortcuts) : base (shortcuts)
+ {
+ Y = 0;
+ Width = Dim.Fill ();
+ Height = Dim.Auto (DimAutoStyle.Content, 1);
+ BorderStyle = LineStyle.Dashed;
+ ColorScheme = Colors.ColorSchemes ["Menu"];
+ Orientation = Orientation.Horizontal;
+
+ LayoutStarted += MenuBarv2_LayoutStarted;
+ }
+
+ // MenuBarv2 arranges the items horizontally.
+ // The first item has no left border, the last item has no right border.
+ // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart).
+ private void MenuBarv2_LayoutStarted (object sender, LayoutEventArgs e)
+ {
+
+ }
+
+ ///
+ public override View Add (View view)
+ {
+ // Call base first, because otherwise it resets CanFocus to true
+ base.Add (view);
+
+ view.CanFocus = true;
+
+ if (view is Shortcut shortcut)
+ {
+ shortcut.KeyBindingScope = KeyBindingScope.Application;
+
+ // TODO: not happy about using AlignmentModes for this. Too implied.
+ // TODO: instead, add a property (a style enum?) to Shortcut to control this
+ //shortcut.AlignmentModes = AlignmentModes.EndToStart;
+
+ shortcut.KeyView.Visible = false;
+ shortcut.HelpView.Visible = false;
+ }
+
+ return view;
+ }
+}
diff --git a/Terminal.Gui/Views/Menuv2.cs b/Terminal.Gui/Views/Menuv2.cs
new file mode 100644
index 000000000..f0024ae28
--- /dev/null
+++ b/Terminal.Gui/Views/Menuv2.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Reflection;
+
+namespace Terminal.Gui;
+
+///
+///
+public class Menuv2 : Bar
+{
+ ///
+ public Menuv2 () : this ([]) { }
+
+ ///
+ public Menuv2 (IEnumerable shortcuts) : base (shortcuts)
+ {
+ Orientation = Orientation.Vertical;
+ Width = Dim.Auto ();
+ Height = Dim.Auto (DimAutoStyle.Content, 1);
+ ColorScheme = Colors.ColorSchemes ["Menu"];
+ Initialized += Menuv2_Initialized;
+ }
+
+ private void Menuv2_Initialized (object sender, EventArgs e)
+ {
+ Border.Thickness = new Thickness (1, 1, 1, 1);
+ }
+
+ // Menuv2 arranges the items horizontally.
+ // The first item has no left border, the last item has no right border.
+ // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart).
+ internal override void OnLayoutStarted (LayoutEventArgs args)
+ {
+ for (int index = 0; index < Subviews.Count; index++)
+ {
+ View barItem = Subviews [index];
+
+ if (!barItem.Visible)
+ {
+ continue;
+ }
+
+ }
+ base.OnLayoutStarted (args);
+ }
+
+ ///
+ public override View Add (View view)
+ {
+ base.Add (view);
+
+ if (view is Shortcut shortcut)
+ {
+ shortcut.CanFocus = true;
+ shortcut.KeyBindingScope = KeyBindingScope.Application;
+ shortcut.Orientation = Orientation.Vertical;
+
+ // TODO: not happy about using AlignmentModes for this. Too implied.
+ // TODO: instead, add a property (a style enum?) to Shortcut to control this
+ //shortcut.AlignmentModes = AlignmentModes.EndToStart;
+ }
+
+ return view;
+ }
+}
diff --git a/Terminal.Gui/Views/OpenDialog.cs b/Terminal.Gui/Views/OpenDialog.cs
index ec7f8eb43..8197671d8 100644
--- a/Terminal.Gui/Views/OpenDialog.cs
+++ b/Terminal.Gui/Views/OpenDialog.cs
@@ -36,7 +36,7 @@ public enum OpenMode
///
///
/// To use, create an instance of , and pass it to
-/// . This will run the dialog modally, and when this returns,
+/// . This will run the dialog modally, and when this returns,
/// the list of files will be available on the property.
///
/// To select more than one file, users can use the spacebar, or control-t.
diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs
index efdfa5260..c87ae9964 100644
--- a/Terminal.Gui/Views/RadioGroup.cs
+++ b/Terminal.Gui/Views/RadioGroup.cs
@@ -25,6 +25,11 @@ public class RadioGroup : View
Command.LineUp,
() =>
{
+ if (!HasFocus)
+ {
+ return false;
+ }
+
MoveUpLeft ();
return true;
@@ -35,6 +40,10 @@ public class RadioGroup : View
Command.LineDown,
() =>
{
+ if (!HasFocus)
+ {
+ return false;
+ }
MoveDownRight ();
return true;
@@ -45,6 +54,10 @@ public class RadioGroup : View
Command.TopHome,
() =>
{
+ if (!HasFocus)
+ {
+ return false;
+ }
MoveHome ();
return true;
@@ -55,6 +68,10 @@ public class RadioGroup : View
Command.BottomEnd,
() =>
{
+ if (!HasFocus)
+ {
+ return false;
+ }
MoveEnd ();
return true;
@@ -355,7 +372,11 @@ public class RadioGroup : View
///
///
public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
- {
+ {
+ if (_selected == selectedItem)
+ {
+ return;
+ }
_selected = selectedItem;
SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem));
}
diff --git a/Terminal.Gui/Views/SaveDialog.cs b/Terminal.Gui/Views/SaveDialog.cs
index f1075159d..61ffb88e4 100644
--- a/Terminal.Gui/Views/SaveDialog.cs
+++ b/Terminal.Gui/Views/SaveDialog.cs
@@ -17,7 +17,7 @@ namespace Terminal.Gui;
///
///
/// To use, create an instance of , and pass it to
-/// . This will run the dialog modally, and when this returns,
+/// . This will run the dialog modally, and when this returns,
/// the property will contain the selected file name or null if the user canceled.
///
///
diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs
index 42a226544..0448d5ef4 100644
--- a/Terminal.Gui/Views/ScrollView.cs
+++ b/Terminal.Gui/Views/ScrollView.cs
@@ -346,7 +346,7 @@ public class ScrollView : View
/// Adds the view to the scrollview.
/// The view to add to the scrollview.
- public override void Add (View view)
+ public override View Add (View view)
{
if (view is ScrollBarView.ContentBottomRightCorner)
{
@@ -365,6 +365,7 @@ public class ScrollView : View
}
SetNeedsLayout ();
+ return view;
}
///
@@ -391,7 +392,7 @@ public class ScrollView : View
return true;
}
- bool? result = InvokeKeyBindings (a);
+ bool? result = InvokeKeyBindings (a, KeyBindingScope.HotKey | KeyBindingScope.Focused);
if (result is { })
{
@@ -456,11 +457,11 @@ public class ScrollView : View
/// Removes the view from the scrollview.
/// The view to remove from the scrollview.
- public override void Remove (View view)
+ public override View Remove (View view)
{
if (view is null)
{
- return;
+ return view;
}
SetNeedsDisplay ();
@@ -479,6 +480,8 @@ public class ScrollView : View
{
CanFocus = false;
}
+
+ return view;
}
/// Removes all widgets from this container.
diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs
new file mode 100644
index 000000000..7290537a0
--- /dev/null
+++ b/Terminal.Gui/Views/Shortcut.cs
@@ -0,0 +1,791 @@
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+///
+/// Displays a command, help text, and a key binding. When the key specified by is pressed, the command will be invoked. Useful for
+/// displaying a command in such as a
+/// menu, toolbar, or status bar.
+///
+///
+///
+/// The following user actions will invoke the , causing the
+/// event to be fired:
+/// - Clicking on the .
+/// - Pressing the key specified by .
+/// - Pressing the HotKey specified by .
+///
+///
+/// If is , will invoked
+/// command regardless of what View has focus, enabling an application-wide keyboard shortcut.
+///
+///
+/// By default, a Shortcut displays the command text on the left side, the help text in the middle, and the key
+/// binding on the
+/// right side. Set to to reverse the order.
+///
+///
+/// The command text can be set by setting the 's Text property or by setting
+/// .
+///
+///
+/// The help text can be set by setting the property or by setting .
+///
+///
+/// The key text is set by setting the property.
+/// If the is , the text is not displayed.
+///
+///
+public class Shortcut : View
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ ///
+ ///
+ /// This is a helper API that mimics the V1 API for creating StatusItems.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Shortcut (Key key, string commandText, Action action, string helpText = null)
+ {
+ Id = "_shortcut";
+ HighlightStyle = HighlightStyle.Pressed;
+ Highlight += Shortcut_Highlight;
+ CanFocus = true;
+ Width = GetWidthDimAuto ();
+ Height = Dim.Auto (DimAutoStyle.Content, 1);
+
+ AddCommand (Command.HotKey, ctx => OnAccept (ctx));
+ AddCommand (Command.Accept, ctx => OnAccept (ctx));
+ AddCommand (Command.Select, ctx => OnSelect (ctx));
+ KeyBindings.Add (KeyCode.Enter, Command.Accept);
+ KeyBindings.Add (KeyCode.Space, Command.Select);
+
+ TitleChanged += Shortcut_TitleChanged; // This needs to be set before CommandView is set
+
+ CommandView = new ()
+ {
+ Width = Dim.Auto (),
+ Height = Dim.Auto ()
+ };
+
+ HelpView.Id = "_helpView";
+ HelpView.CanFocus = false;
+ HelpView.Text = helpText;
+ Add (HelpView);
+
+ KeyView.Id = "_keyView";
+ KeyView.CanFocus = false;
+ Add (KeyView);
+
+ // If the user clicks anywhere on the Shortcut, other than the CommandView, invoke the Command
+ MouseClick += Shortcut_MouseClick;
+ HelpView.MouseClick += Shortcut_MouseClick;
+ KeyView.MouseClick += Shortcut_MouseClick;
+ LayoutStarted += OnLayoutStarted;
+ Initialized += OnInitialized;
+
+ if (key is null)
+ {
+ key = Key.Empty;
+ }
+
+ Key = key;
+ Title = commandText;
+ Action = action;
+
+ return;
+
+ void OnInitialized (object sender, EventArgs e)
+ {
+ SuperViewRendersLineCanvas = true;
+ Border.ShowTitle = false;
+
+ ShowHide ();
+
+ // Force Width to DimAuto to calculate natural width and then set it back
+ Dim savedDim = Width;
+ Width = GetWidthDimAuto ();
+ _minimumDimAutoWidth = Frame.Width;
+ Width = savedDim;
+
+ SetCommandViewDefaultLayout ();
+ SetHelpViewDefaultLayout ();
+ SetKeyViewDefaultLayout ();
+
+ SetColors ();
+ }
+
+ // Helper to set Width consistently
+ Dim GetWidthDimAuto ()
+ {
+ // TODO: PosAlign.CalculateMinDimension is a hack. Need to figure out a better way of doing this.
+ return Dim.Auto (
+ DimAutoStyle.Content,
+ Dim.Func (() => PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width)),
+ Dim.Func (() => PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width)));
+ }
+ }
+
+
+ ///
+ /// Creates a new instance of .
+ ///
+ public Shortcut () : this (Key.Empty, string.Empty, null) { }
+
+ private Orientation _orientation = Orientation.Horizontal;
+
+ ///
+ /// Gets or sets the for this . The default is
+ /// , which is ideal for status bar, menu bar, and tool bar items If set to
+ /// ,
+ /// the Shortcut will be configured for vertical layout, which is ideal for menu items.
+ ///
+ public Orientation Orientation
+ {
+ get => _orientation;
+ set
+ {
+ _orientation = value;
+
+ // TODO: Determine what, if anything, is opinionated about the orientation.
+ }
+ }
+
+ private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast;
+
+ ///
+ /// Gets or sets the for this .
+ ///
+ ///
+ ///
+ /// The default is . This means that the CommandView will be on the left,
+ /// HelpView in the middle, and KeyView on the right.
+ ///
+ ///
+ public AlignmentModes AlignmentModes
+ {
+ get => _alignmentModes;
+ set
+ {
+ _alignmentModes = value;
+ SetCommandViewDefaultLayout ();
+ SetHelpViewDefaultLayout ();
+ SetKeyViewDefaultLayout ();
+ }
+ }
+
+ // When one of the subviews is "empty" we don't want to show it. So we
+ // Use Add/Remove. We need to be careful to add them in the right order
+ // so Pos.Align works correctly.
+ internal void ShowHide ()
+ {
+ RemoveAll ();
+
+ if (CommandView.Visible)
+ {
+ Add (CommandView);
+ }
+
+ if (HelpView.Visible && !string.IsNullOrEmpty (HelpView.Text))
+ {
+ Add (HelpView);
+ }
+
+ if (KeyView.Visible && Key != Key.Empty)
+ {
+ Add (KeyView);
+ }
+ }
+
+ // This is used to calculate the minimum width of the Shortcut when the width is NOT Dim.Auto
+ private int? _minimumDimAutoWidth;
+
+ // When layout starts, we need to adjust the layout of the HelpView and KeyView
+ private void OnLayoutStarted (object sender, LayoutEventArgs e)
+ {
+ if (Width is DimAuto widthAuto)
+ {
+ _minimumDimAutoWidth = Frame.Width;
+ }
+ else
+ {
+ if (string.IsNullOrEmpty (HelpView.Text))
+ {
+ return;
+ }
+
+ int currentWidth = Frame.Width;
+
+ // If our width is smaller than the natural width then reduce width of HelpView first.
+ // Then KeyView.
+ // Don't ever reduce CommandView (it should spill).
+ // When Horizontal, Key is first, then Help, then Command.
+ // When Vertical, Command is first, then Help, then Key.
+ // BUGBUG: This does not do what the above says.
+ // TODO: Add Unit tests for this.
+ if (currentWidth < _minimumDimAutoWidth)
+ {
+ int delta = _minimumDimAutoWidth.Value - currentWidth;
+ int maxHelpWidth = int.Max (0, HelpView.Text.GetColumns () + Margin.Thickness.Horizontal - delta);
+
+ switch (maxHelpWidth)
+ {
+ case 0:
+ // Hide HelpView
+ HelpView.Visible = false;
+ HelpView.X = 0;
+
+ break;
+
+ case 1:
+ // Scrunch it by removing margins
+ HelpView.Margin.Thickness = new (0, 0, 0, 0);
+
+ break;
+
+ case 2:
+ // Scrunch just the right margin
+ Thickness t = GetMarginThickness ();
+ HelpView.Margin.Thickness = new (t.Right, t.Top, t.Left - 1, t.Bottom);
+
+ break;
+
+ default:
+ // Default margin
+ HelpView.Margin.Thickness = GetMarginThickness ();
+
+ break;
+ }
+
+ if (maxHelpWidth > 0)
+ {
+ HelpView.X = Pos.Align (Alignment.End, AlignmentModes);
+
+ // Leverage Dim.Auto's max:
+ HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: maxHelpWidth);
+ HelpView.Visible = true;
+ }
+ }
+ else
+ {
+ // Reset to default
+ //SetCommandViewDefaultLayout();
+ SetHelpViewDefaultLayout ();
+
+ //SetKeyViewDefaultLayout ();
+ }
+ }
+ }
+
+ private Thickness GetMarginThickness ()
+ {
+ if (Orientation == Orientation.Vertical)
+ {
+ return new (1, 0, 1, 0);
+ }
+
+ return new (1, 0, 1, 0);
+ }
+
+ private Color? _savedForeColor;
+
+ private void Shortcut_Highlight (object sender, HighlightEventArgs e)
+ {
+ if (e.HighlightStyle.HasFlag (HighlightStyle.Pressed))
+ {
+ if (!_savedForeColor.HasValue)
+ {
+ _savedForeColor = base.ColorScheme.Normal.Foreground;
+ }
+
+ var cs = new ColorScheme (base.ColorScheme)
+ {
+ Normal = new (ColorScheme.Normal.Foreground.GetHighlightColor (), base.ColorScheme.Normal.Background)
+ };
+ base.ColorScheme = cs;
+ }
+
+ if (e.HighlightStyle == HighlightStyle.None && _savedForeColor.HasValue)
+ {
+ var cs = new ColorScheme (base.ColorScheme)
+ {
+ Normal = new (_savedForeColor.Value, base.ColorScheme.Normal.Background)
+ };
+ base.ColorScheme = cs;
+ }
+
+ SuperView?.SetNeedsDisplay ();
+ e.Cancel = true;
+ }
+
+ private void Shortcut_MouseClick (object sender, MouseEventEventArgs e)
+ {
+ // When the Shortcut is clicked, we want to invoke the Command and Set focus
+ var view = sender as View;
+
+ if (!e.Handled)
+ {
+ // If the subview (likely CommandView) didn't handle the mouse click, invoke the command.
+ e.Handled = InvokeCommand (Command.Accept) == true;
+ }
+
+ if (CanFocus)
+ {
+ SetFocus ();
+ }
+ }
+
+ #region Command
+
+ private View _commandView = new ();
+
+ ///
+ /// Gets or sets the View that displays the command text and hotkey.
+ ///
+ ///
+ ///
+ /// By default, the of the is displayed as the Shortcut's
+ /// command text.
+ ///
+ ///
+ /// By default, the CommandView is a with set to
+ /// .
+ ///
+ ///
+ /// Setting the will add it to the and remove any existing
+ /// .
+ ///
+ ///
+ ///
+ ///
+ /// This example illustrates how to add a to a that toggles the
+ /// property.
+ ///
+ ///
+ /// var force16ColorsShortcut = new Shortcut
+ /// {
+ /// Key = Key.F6,
+ /// KeyBindingScope = KeyBindingScope.HotKey,
+ /// CommandView = new CheckBox { Text = "Force 16 Colors" }
+ /// };
+ /// var cb = force16ColorsShortcut.CommandView as CheckBox;
+ /// cb.Checked = Application.Force16Colors;
+ ///
+ /// cb.Toggled += (s, e) =>
+ /// {
+ /// var cb = s as CheckBox;
+ /// Application.Force16Colors = cb!.Checked == true;
+ /// Application.Refresh();
+ /// };
+ /// StatusBar.Add(force16ColorsShortcut);
+ ///
+ ///
+
+ public View CommandView
+ {
+ get => _commandView;
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException ();
+ }
+
+ if (_commandView is { })
+ {
+ Remove (_commandView);
+ _commandView?.Dispose ();
+ }
+
+ _commandView = value;
+ _commandView.Id = "_commandView";
+
+ // The default behavior is for CommandView to not get focus. I
+ // If you want it to get focus, you need to set it.
+ _commandView.CanFocus = false;
+
+ _commandView.MouseClick += Shortcut_MouseClick;
+ _commandView.Accept += CommandViewAccept;
+
+ _commandView.HotKeyChanged += (s, e) =>
+ {
+ if (e.NewKey != Key.Empty)
+ {
+ // Add it
+ AddKeyBindingsForHotKey (e.OldKey, e.NewKey);
+ }
+ };
+
+ _commandView.HotKeySpecifier = new ('_');
+
+ Title = _commandView.Text;
+
+ SetCommandViewDefaultLayout ();
+ SetHelpViewDefaultLayout ();
+ SetKeyViewDefaultLayout ();
+ ShowHide ();
+ UpdateKeyBinding ();
+
+ return;
+
+ void CommandViewAccept (object sender, CancelEventArgs e)
+ {
+ // When the CommandView fires its Accept event, we want to act as though the
+ // Shortcut was clicked.
+ //if (base.OnAccept () == true)
+ //{
+ // e.Cancel = true;
+ //}
+ }
+ }
+ }
+
+ private void SetCommandViewDefaultLayout ()
+ {
+ CommandView.Margin.Thickness = GetMarginThickness ();
+ CommandView.X = Pos.Align (Alignment.End, AlignmentModes);
+ CommandView.Y = 0; //Pos.Center ();
+ }
+
+ private void Shortcut_TitleChanged (object sender, StateEventArgs e)
+ {
+ // If the Title changes, update the CommandView text.
+ // This is a helper to make it easier to set the CommandView text.
+ // CommandView is public and replaceable, but this is a convenience.
+ _commandView.Text = Title;
+ }
+
+ #endregion Command
+
+ #region Help
+
+ ///
+ /// The subview that displays the help text for the command. Internal for unit testing.
+ ///
+ internal View HelpView { get; } = new ();
+
+ private void SetHelpViewDefaultLayout ()
+ {
+ HelpView.Margin.Thickness = GetMarginThickness ();
+ HelpView.X = Pos.Align (Alignment.End, AlignmentModes);
+ HelpView.Y = 0; //Pos.Center ();
+ HelpView.Width = Dim.Auto (DimAutoStyle.Text);
+ HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1;
+
+ HelpView.Visible = true;
+ HelpView.VerticalTextAlignment = Alignment.Center;
+ }
+
+ ///
+ /// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to
+ /// .
+ ///
+ public override string Text
+ {
+ get => HelpView?.Text;
+ set
+ {
+ if (HelpView != null)
+ {
+ HelpView.Text = value;
+ ShowHide ();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the help text displayed in the middle of the Shortcut.
+ ///
+ public string HelpText
+ {
+ get => HelpView?.Text;
+ set
+ {
+ if (HelpView != null)
+ {
+ HelpView.Text = value;
+ ShowHide ();
+ }
+ }
+ }
+
+ #endregion Help
+
+ #region Key
+
+ private Key _key = Key.Empty;
+
+ ///
+ /// Gets or sets the that will be bound to the command.
+ ///
+ public Key Key
+ {
+ get => _key;
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException ();
+ }
+
+ _key = value;
+
+ UpdateKeyBinding ();
+
+ KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
+ ShowHide ();
+ }
+ }
+
+ private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
+
+ ///
+ /// Gets or sets the scope for the key binding for how is bound to .
+ ///
+ public KeyBindingScope KeyBindingScope
+ {
+ get => _keyBindingScope;
+ set
+ {
+ _keyBindingScope = value;
+
+ UpdateKeyBinding ();
+ }
+ }
+
+ ///
+ /// Gets the subview that displays the key. Internal for unit testing.
+ ///
+
+ internal View KeyView { get; } = new ();
+
+ private int _minimumKeyTextSize;
+
+ ///
+ /// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s.
+ ///
+ public int MinimumKeyTextSize
+ {
+ get => _minimumKeyTextSize;
+ set
+ {
+ if (value == _minimumKeyTextSize)
+ {
+ //return;
+ }
+
+ _minimumKeyTextSize = value;
+ SetKeyViewDefaultLayout ();
+ CommandView.SetNeedsLayout ();
+ HelpView.SetNeedsLayout ();
+ KeyView.SetNeedsLayout ();
+ SetSubViewNeedsDisplay ();
+ }
+ }
+
+ private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; }
+
+ private void SetKeyViewDefaultLayout ()
+ {
+ KeyView.Margin.Thickness = GetMarginThickness ();
+ KeyView.X = Pos.Align (Alignment.End, AlignmentModes);
+ KeyView.Y = 0; //Pos.Center ();
+ KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize));
+ KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1;
+
+ KeyView.Visible = true;
+
+ // Right align the text in the keyview
+ KeyView.TextAlignment = Alignment.End;
+ KeyView.VerticalTextAlignment = Alignment.Center;
+ KeyView.KeyBindings.Clear ();
+ }
+
+ private void UpdateKeyBinding ()
+ {
+ if (Key != null)
+ {
+ // Disable the command view key bindings
+ CommandView.KeyBindings.Remove (Key);
+ CommandView.KeyBindings.Remove (CommandView.HotKey);
+ KeyBindings.Remove (Key);
+ KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept);
+ //KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept);
+ }
+ }
+
+ #endregion Key
+
+ #region Accept Handling
+
+ ///
+ /// Called when the command is received. This
+ /// occurs
+ /// - if the user clicks anywhere on the shortcut with the mouse
+ /// - if the user presses Key
+ /// - if the user presses the HotKey specified by CommandView
+ /// - if HasFocus and the user presses Space or Enter (or any other key bound to Command.Accept).
+ ///
+ protected new bool? OnAccept (CommandContext ctx)
+ {
+ var cancel = false;
+
+ switch (ctx.KeyBinding?.Scope)
+ {
+ case KeyBindingScope.Application:
+ cancel = base.OnAccept () == true;
+
+ break;
+
+ case KeyBindingScope.Focused:
+ // TODO: Figure this out
+ cancel = base.OnAccept () == true;
+
+ break;
+
+ case KeyBindingScope.HotKey:
+ cancel = base.OnAccept () == true;
+
+ if (CanFocus)
+ {
+ SetFocus ();
+ }
+
+ break;
+
+ default:
+ cancel = base.OnAccept () == true;
+ break;
+ }
+
+ CommandView.InvokeCommand (Command.Accept);
+
+ if (!cancel)
+ {
+ Action?.Invoke ();
+ }
+
+ return cancel;
+ }
+
+ ///
+ /// Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the
+ /// mouse.
+ ///
+ ///
+ /// Note, the event is fired first, and if cancelled, the event will not be invoked.
+ ///
+ [CanBeNull]
+ public Action Action { get; set; }
+
+ #endregion Accept Handling
+
+ private bool? OnSelect (CommandContext ctx)
+ {
+ if (CommandView.GetSupportedCommands ().Contains (Command.Select))
+ {
+ return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
+ }
+ return false;
+
+ }
+
+
+ #region Focus
+
+ ///
+ public override ColorScheme ColorScheme
+ {
+ get => base.ColorScheme;
+ set
+ {
+ base.ColorScheme = value;
+ SetColors ();
+ }
+ }
+
+ ///
+ ///
+ internal void SetColors ()
+ {
+ // Border should match superview.
+ Border.ColorScheme = SuperView?.ColorScheme;
+
+ if (HasFocus)
+ {
+ // When we have focus, we invert the colors
+ base.ColorScheme = new (base.ColorScheme)
+ {
+ Normal = base.ColorScheme.Focus,
+ HotNormal = base.ColorScheme.HotFocus,
+ HotFocus = base.ColorScheme.HotNormal,
+ Focus = base.ColorScheme.Normal
+ };
+ }
+ else
+ {
+ base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme;
+ }
+
+ // Set KeyView's colors to show "hot"
+ if (IsInitialized && base.ColorScheme is { })
+ {
+ var cs = new ColorScheme (base.ColorScheme)
+ {
+ Normal = base.ColorScheme.HotNormal,
+ HotNormal = base.ColorScheme.Normal
+ };
+ KeyView.ColorScheme = cs;
+ }
+ }
+
+ View _lastFocusedView;
+ ///
+ public override bool OnEnter (View view)
+ {
+ SetColors ();
+ _lastFocusedView = view;
+
+ return base.OnEnter (view);
+ }
+
+ ///
+ public override bool OnLeave (View view)
+ {
+ SetColors ();
+ _lastFocusedView = this;
+
+ return base.OnLeave (view);
+ }
+
+ #endregion Focus
+
+ ///
+ protected override void Dispose (bool disposing)
+ {
+ if (disposing)
+ {
+ if (CommandView?.IsAdded == false)
+ {
+ CommandView.Dispose ();
+ }
+
+ if (HelpView?.IsAdded == false)
+ {
+ HelpView.Dispose ();
+ }
+
+ if (KeyView?.IsAdded == false)
+ {
+ KeyView.Dispose ();
+ }
+ }
+
+ base.Dispose (disposing);
+ }
+}
diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs
index c42247299..6dca6f8c4 100644
--- a/Terminal.Gui/Views/Slider.cs
+++ b/Terminal.Gui/Views/Slider.cs
@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using System.Transactions;
+
+namespace Terminal.Gui;
/// Slider control.
public class Slider : Slider