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 @@ -1377,7 +1379,8 @@ public class Slider : View SetNeedsDisplay (); - return true; + mouseEvent.Handled = true; + return OnMouseClick (new (mouseEvent)); } return false; @@ -1417,7 +1420,8 @@ public class Slider : View AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.RightExtend, () => ExtendPlus ()); AddCommand (Command.LeftExtend, () => ExtendMinus ()); - AddCommand (Command.Accept, () => Set ()); + AddCommand (Command.Select, () => Select ()); + AddCommand (Command.Accept, () => Accept ()); SetKeyBindings (); } @@ -1453,7 +1457,7 @@ public class Slider : View KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.Enter, Command.Accept); - KeyBindings.Add (Key.Space, Command.Accept); + KeyBindings.Add (Key.Space, Command.Select); } private Dictionary> GetSetOptionDictionary () { return _setOptions.ToDictionary (e => e, e => _options [e]); } @@ -1732,11 +1736,18 @@ public class Slider : View return true; } - internal bool Set () + internal bool Select () + { + SetFocusedOption(); + + return true; + } + + internal bool Accept () { SetFocusedOption (); - return true; + return OnAccept () == true; } internal bool MovePlus () diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 4694eebfb..b4df14e6b 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -1,243 +1,77 @@ +using System; +using System.Reflection; + namespace Terminal.Gui; /// /// A status bar is a that snaps to the bottom of a displaying set of -/// s. The should be context sensitive. This means, if the main menu +/// s. The should be context sensitive. This means, if the main menu /// and an open text editor are visible, the items probably shown will be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog /// to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a /// new instance of a status bar. /// -public class StatusBar : View +public class StatusBar : Bar { - private static Rune _shortcutDelimiter = (Rune)'='; + /// + public StatusBar () : this ([]) { } - private StatusItem [] _items = []; - - /// Initializes a new instance of the class. - public StatusBar () : this (new StatusItem [] { }) { } - - /// - /// Initializes a new instance of the class with the specified set of - /// s. The will be drawn on the lowest line of the terminal or - /// (if not null). - /// - /// A list of status bar items. - public StatusBar (StatusItem [] items) + /// + public StatusBar (IEnumerable shortcuts) : base (shortcuts) { - if (items is { }) - { - Items = items; - } - - CanFocus = false; - ColorScheme = Colors.ColorSchemes ["Menu"]; - X = 0; + Orientation = Orientation.Horizontal; Y = Pos.AnchorEnd (); Width = Dim.Fill (); - Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize (). + Height = Dim.Auto (DimAutoStyle.Content, 1); + BorderStyle = LineStyle.Dashed; + ColorScheme = Colors.ColorSchemes ["Menu"]; - AddCommand (Command.Accept, ctx => InvokeItem ((StatusItem)ctx.KeyBinding?.Context)); + LayoutStarted += StatusBar_LayoutStarted; } - /// The items that compose the - public StatusItem [] Items + // StatusBar 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 StatusBar_LayoutStarted (object sender, LayoutEventArgs e) { - get => _items; - set + for (int index = 0; index < Subviews.Count; index++) { - foreach (StatusItem item in _items) + View barItem = Subviews [index]; + + barItem.BorderStyle = BorderStyle; + + if (index == Subviews.Count - 1) { - KeyBindings.Remove (item.Shortcut); + barItem.Border.Thickness = new Thickness (0, 0, 0, 0); + } + else + { + barItem.Border.Thickness = new Thickness (0, 0, 1, 0); } - _items = value; - - foreach (StatusItem item in _items.Where (i => i.Shortcut != Key.Empty)) + if (barItem is Shortcut shortcut) { - KeyBinding keyBinding = new (new [] { Command.Accept }, KeyBindingScope.HotKey, item); - KeyBindings.Add (item.Shortcut, keyBinding); + shortcut.Orientation = Orientation.Horizontal; } } } - /// Gets or sets shortcut delimiter separator. The default is "-". - public static Rune ShortcutDelimiter + /// + public override View Add (View view) { - get => _shortcutDelimiter; - set - { - if (_shortcutDelimiter != value) - { - _shortcutDelimiter = value == default (Rune) ? (Rune)'=' : value; - } - } - } + // Call base first, because otherwise it resets CanFocus to true + base.Add (view); - /// Inserts a in the specified index of . - /// The zero-based index at which item should be inserted. - /// The item to insert. - public void AddItemAt (int index, StatusItem item) - { - List itemsList = new (Items); - itemsList.Insert (index, item); - Items = itemsList.ToArray (); - SetNeedsDisplay (); - } + view.CanFocus = false; - /// - protected internal override bool OnMouseEvent (MouseEvent me) - { - if (me.Flags != MouseFlags.Button1Clicked) + if (view is Shortcut shortcut) { - return false; + 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; } - var pos = 1; - - for (var i = 0; i < Items.Length; i++) - { - if (me.Position.X >= pos && me.Position.X < pos + GetItemTitleLength (Items [i].Title)) - { - StatusItem item = Items [i]; - - if (item.IsEnabled ()) - { - Run (item.Action); - } - - break; - } - - pos += GetItemTitleLength (Items [i].Title) + 3; - } - - return true; - } - - /// - public override void OnDrawContent (Rectangle viewport) - { - Move (0, 0); - Driver.SetAttribute (GetNormalColor ()); - - for (var i = 0; i < Frame.Width; i++) - { - Driver.AddRune ((Rune)' '); - } - - Move (1, 0); - Attribute scheme = GetNormalColor (); - Driver.SetAttribute (scheme); - - for (var i = 0; i < Items.Length; i++) - { - string title = Items [i].Title; - Driver.SetAttribute (DetermineColorSchemeFor (Items [i])); - - for (var n = 0; n < Items [i].Title.GetRuneCount (); n++) - { - if (title [n] == '~') - { - if (Items [i].IsEnabled ()) - { - scheme = ToggleScheme (scheme); - } - - continue; - } - - Driver.AddRune ((Rune)title [n]); - } - - if (i + 1 < Items.Length) - { - Driver.AddRune ((Rune)' '); - Driver.AddRune (Glyphs.VLine); - Driver.AddRune ((Rune)' '); - } - } - } - - /// Removes a at specified index of . - /// The zero-based index of the item to remove. - /// The removed. - public StatusItem RemoveItem (int index) - { - List itemsList = new (Items); - StatusItem item = itemsList [index]; - itemsList.RemoveAt (index); - Items = itemsList.ToArray (); - SetNeedsDisplay (); - - return item; - } - - private Attribute DetermineColorSchemeFor (StatusItem item) - { - if (item is { }) - { - if (item.IsEnabled ()) - { - return GetNormalColor (); - } - - return ColorScheme.Disabled; - } - - return GetNormalColor (); - } - - private int GetItemTitleLength (string title) - { - var len = 0; - - foreach (char ch in title) - { - if (ch == '~') - { - continue; - } - - len++; - } - - return len; - } - - private bool? InvokeItem (StatusItem itemToInvoke) - { - if (itemToInvoke is { Action: { } }) - { - itemToInvoke.Action.Invoke (); - - return true; - } - - return false; - } - - private void Run (Action action) - { - if (action is null) - { - return; - } - - Application.MainLoop.AddIdle ( - () => - { - action (); - - return false; - } - ); - } - - private Attribute ToggleScheme (Attribute scheme) - { - Attribute result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; - Driver.SetAttribute (result); - - return result; + return view; } } diff --git a/Terminal.Gui/Views/StatusItem.cs b/Terminal.Gui/Views/StatusItem.cs deleted file mode 100644 index 5028f722e..000000000 --- a/Terminal.Gui/Views/StatusItem.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Terminal.Gui; - -/// -/// objects are contained by s. Each -/// has a title, a shortcut (hotkey), and an that will be invoked when -/// the is pressed. The will be a global hotkey for -/// the application in the current context of the screen. The color of the will be -/// changed after each ~. A set to `~F1~ Help` will render as *F1* using -/// and *Help* as . -/// -public class StatusItem -{ - /// Initializes a new . - /// Shortcut to activate the . - /// Title for the . - /// Action to invoke when the is activated. - /// Function to determine if the action can currently be executed. - public StatusItem (Key shortcut, string title, Action action, Func canExecute = null) - { - Title = title ?? ""; - Shortcut = shortcut; - Action = action; - CanExecute = canExecute; - } - - /// Gets or sets the action to be invoked when the is triggered - /// Action to invoke. - public Action Action { get; set; } - - /// - /// Gets or sets the action to be invoked to determine if the can be triggered. If - /// returns the status item will be enabled. Otherwise, it will be - /// disabled. - /// - /// Function to determine if the action is can be executed or not. - public Func CanExecute { get; set; } - - /// Gets or sets arbitrary data for the status item. - /// This property is not used internally. - public object Data { get; set; } - - /// Gets the global shortcut to invoke the action on the menu. - public Key Shortcut { get; set; } - - /// Gets or sets the title. - /// The title. - /// - /// The colour of the will be changed after each ~. A - /// set to `~F1~ Help` will render as *F1* using and - /// *Help* as . - /// - public string Title { get; set; } - - /// - /// Returns if the status item is enabled. This method is a wrapper around - /// . - /// - public bool IsEnabled () { return CanExecute?.Invoke () ?? true; } -} diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 3323e4541..7b808d1ea 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -1034,7 +1034,7 @@ public class TextField : View } /// - public override bool? OnInvokingKeyBindings (Key a) + public override bool? OnInvokingKeyBindings (Key a, KeyBindingScope scope) { // Give autocomplete first opportunity to respond to key presses if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (a)) @@ -1042,7 +1042,7 @@ public class TextField : View return true; } - return base.OnInvokingKeyBindings (a); + return base.OnInvokingKeyBindings (a, scope); } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 8b4f17424..d9c67cf14 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1961,7 +1961,6 @@ public class TextView : View private readonly HistoryText _historyText = new (); private bool _allowsReturn = true; private bool _allowsTab = true; - private int _bottomOffset, _rightOffset; private bool _clickWithSelecting; // The column we are tracking, or -1 if we are not tracking any column @@ -3635,7 +3634,7 @@ public class TextView : View } /// - public override bool? OnInvokingKeyBindings (Key a) + public override bool? OnInvokingKeyBindings (Key a, KeyBindingScope scope) { if (!a.IsValid) { @@ -3648,7 +3647,7 @@ public class TextView : View return true; } - return base.OnInvokingKeyBindings (a); + return base.OnInvokingKeyBindings (a, scope); } /// diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 117599efb..617dfa7fc 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui; /// /// /// Toplevels can run as modal (popup) views, started by calling -/// . They return control to the caller when +/// . They return control to the caller when /// has been called (which sets the /// property to false). /// @@ -15,7 +15,7 @@ namespace Terminal.Gui; /// A Toplevel is created when an application initializes Terminal.Gui by calling . /// The application Toplevel can be accessed via . Additional Toplevels can be created /// and run (e.g. s. To run a Toplevel, create the and call -/// . +/// . /// /// public partial class Toplevel : View @@ -106,7 +106,7 @@ public partial class Toplevel : View ); // Default keybindings for this view - KeyBindings.Add (Application.QuitKey, KeyBindingScope.Application, Command.QuitToplevel); + KeyBindings.Add (Application.QuitKey, Command.QuitToplevel); KeyBindings.Add (Key.CursorRight, Command.NextView); KeyBindings.Add (Key.CursorDown, Command.NextView); @@ -186,11 +186,11 @@ public partial class Toplevel : View public event EventHandler Activate; /// - public override void Add (View view) + public override View Add (View view) { CanFocus = true; AddMenuStatusBar (view); - base.Add (view); + return base.Add (view); } /// @@ -445,20 +445,20 @@ public partial class Toplevel : View /// perform tasks when the has been laid out and focus has been set. changes. /// /// A Ready event handler is a good place to finalize initialization after calling - /// on this . + /// on this . /// /// public event EventHandler Ready; /// - public override void Remove (View view) + public override View Remove (View view) { if (this is Toplevel { MenuBar: { } }) { RemoveMenuStatusBar (view); } - base.Remove (view); + return base.Remove (view); } /// diff --git a/Terminal.Gui/Views/ToplevelOverlapped.cs b/Terminal.Gui/Views/ToplevelOverlapped.cs index a1142b5df..0d93ef79f 100644 --- a/Terminal.Gui/Views/ToplevelOverlapped.cs +++ b/Terminal.Gui/Views/ToplevelOverlapped.cs @@ -67,7 +67,7 @@ public static partial class Application View top = FindTopFromView (Top?.MostFocused); - if (top is Toplevel && Top.Subviews.Count > 1 && Top.Subviews [Top.Subviews.Count - 1] != top) + if (top is Toplevel && Top.Subviews.Count > 1 && Top.Subviews [^1] != top) { Top.BringSubviewToFront (top); } diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index ab81e515a..1f11036c1 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -125,7 +125,7 @@ public class Wizard : Dialog /// Add the Wizard to a containing view with . /// /// - /// If a non-Modal Wizard is added to the application after has + /// If a non-Modal Wizard is added to the application after has /// been called the first step must be explicitly set by setting to /// : /// diff --git a/Terminal.Gui/Views/Wizard/WizardStep.cs b/Terminal.Gui/Views/Wizard/WizardStep.cs index c28c3653c..f6ebac473 100644 --- a/Terminal.Gui/Views/Wizard/WizardStep.cs +++ b/Terminal.Gui/Views/Wizard/WizardStep.cs @@ -126,7 +126,7 @@ public class WizardStep : FrameView /// Add the specified to the . /// to add to this container - public override void Add (View view) + public override View Add (View view) { _contentView.Add (view); @@ -136,15 +136,17 @@ public class WizardStep : FrameView } ShowHide (); + + return view; } /// Removes a from . /// - public override void Remove (View view) + public override View Remove (View view) { if (view is null) { - return; + return view; } SetNeedsDisplay (); @@ -165,6 +167,8 @@ public class WizardStep : FrameView } ShowHide (); + + return view; } /// Removes all s from the . diff --git a/Terminal.sln b/Terminal.sln index 98de83b41..ee838a41a 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui.Analyzers.Inte EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui.Analyzers.Internal.Debugging", "Analyzers\Terminal.Gui.Analyzers.Internal.Debugging\Terminal.Gui.Analyzers.Internal.Debugging.csproj", "{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkitExample", "CommunityToolkitExample\CommunityToolkitExample.csproj", "{58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}" +EndProject Global GlobalSection(NestedProjects) = preSolution {5DE91722-8765-4E2B-97E4-2A18010B5CED} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D} @@ -81,6 +83,10 @@ Global {C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Release|Any CPU.Build.0 = Release|Any CPU + {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index 47467caba..df8e510d9 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -188,6 +188,8 @@ public class Scenario : IDisposable { // Must explicitly call Application.Shutdown method to shutdown. Application.Run (Top); + Top.Dispose (); + Application.Shutdown (); } /// Override this to implement the setup logic (create controls, etc...). diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs index 5eb586aa1..59e8924f0 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -59,6 +59,8 @@ public class ASCIICustomButtonTest : Scenario Application.Run (top); top.Dispose (); + Application.Shutdown (); + return; void ChangeWindowSize () diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index e6e35debb..b2f6cdfa9 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -88,25 +88,23 @@ public class BackgroundWorkerCollection : Scenario ; _menu.MenuOpening += Menu_MenuOpening; Add (_menu); - var statusBar = new StatusBar ( new [] { - new StatusItem (Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit ()), - new StatusItem ( - KeyCode.CtrlMask | KeyCode.R, - "~^R~ Run Worker", - () => _workerApp.RunWorker () - ), - new StatusItem ( - KeyCode.CtrlMask | KeyCode.C, - "~^C~ Cancel Worker", - () => _workerApp.CancelWorker () - ) + new Shortcut (Application.QuitKey, $"Quit", Quit), + new Shortcut ( + Key.R.WithCtrl, + "Run Worker", + () => _workerApp.RunWorker () + ), + new Shortcut ( + Key.C.WithCtrl, + "Cancel Worker", + () => _workerApp.CancelWorker () + ) } ); Add (statusBar); - Ready += OverlappedMain_Ready; Activate += OverlappedMain_Activate; Deactivate += OverlappedMain_Deactivate; diff --git a/UICatalog/Scenarios/Bars.cs b/UICatalog/Scenarios/Bars.cs new file mode 100644 index 000000000..baf98a65d --- /dev/null +++ b/UICatalog/Scenarios/Bars.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("Bars", "Illustrates Bar views (e.g. StatusBar)")] +[ScenarioCategory ("Controls")] +public class Bars : Scenario +{ + public override void Main () + { + Application.Init (); + Toplevel app = new (); + + app.Loaded += App_Loaded; + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); + } + + + // Setting everything up in Loaded handler because we change the + // QuitKey and it only sticks if changed after init + private void App_Loaded (object sender, EventArgs e) + { + Application.Top.Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"; + + ObservableCollection eventSource = new (); + ListView eventLog = new ListView () + { + Title = "Event Log", + X = Pos.AnchorEnd (), + Width = Dim.Auto (), + Height = Dim.Fill (), // Make room for some wide things + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Source = new ListWrapper (eventSource) + }; + eventLog.Border.Thickness = new (0, 1, 0, 0); + Application.Top.Add (eventLog); + + FrameView menuBarLikeExamples = new () + { + Title = "MenuBar-Like Examples", + X = 0, + Y = 0, + Width = Dim.Fill () - Dim.Width (eventLog), + Height = Dim.Percent(33), + }; + Application.Top.Add (menuBarLikeExamples); + + Label label = new Label () + { + Title = " Bar:", + X = 0, + Y = 0, + }; + menuBarLikeExamples.Add (label); + + Bar bar = new Bar + { + Id = "menuBar-like", + X = Pos.Right (label), + Y = Pos.Top (label), + Width = Dim.Fill (), + }; + + ConfigMenuBar (bar); + menuBarLikeExamples.Add (bar); + + label = new Label () + { + Title = " MenuBar:", + X = 0, + Y = Pos.Bottom (bar) + 1 + }; + menuBarLikeExamples.Add (label); + + bar = new MenuBarv2 + { + Id = "menuBar", + X = Pos.Right (label), + Y = Pos.Top (label), + }; + + ConfigMenuBar (bar); + menuBarLikeExamples.Add (bar); + + FrameView menuLikeExamples = new () + { + Title = "Menu-Like Examples", + X = 0, + Y = Pos.Center (), + Width = Dim.Fill () - Dim.Width (eventLog), + Height = Dim.Percent (33), + }; + Application.Top.Add (menuLikeExamples); + + label = new Label () + { + Title = "Bar:", + X = 0, + Y = 0, + }; + menuLikeExamples.Add (label); + + bar = new Bar + { + Id = "menu-like", + X = 0, + Y = Pos.Bottom(label), + //Width = Dim.Percent (40), + Orientation = Orientation.Vertical, + }; + ConfigureMenu (bar); + + menuLikeExamples.Add (bar); + + label = new Label () + { + Title = "Menu:", + X = Pos.Right(bar) + 1, + Y = Pos.Top (label), + }; + menuLikeExamples.Add (label); + + bar = new Menuv2 + { + Id = "menu", + X = Pos.Left (label), + Y = Pos.Bottom (label), + }; + ConfigureMenu (bar); + + menuLikeExamples.Add (bar); + + FrameView statusBarLikeExamples = new () + { + Title = "StatusBar-Like Examples", + X = 0, + Y = Pos.AnchorEnd (), + Width = Dim.Width (menuLikeExamples), + Height = Dim.Percent (33), + }; + Application.Top.Add (statusBarLikeExamples); + + label = new Label () + { + Title = " Bar:", + X = 0, + Y = 0, + }; + statusBarLikeExamples.Add (label); + bar = new Bar + { + Id = "statusBar-like", + X = Pos.Right (label), + Y = Pos.Top (label), + Width = Dim.Fill (), + Orientation = Orientation.Horizontal, + }; + ConfigStatusBar (bar); + statusBarLikeExamples.Add (bar); + + label = new Label () + { + Title = "StatusBar:", + X = 0, + Y = Pos.Bottom (bar) + 1, + }; + statusBarLikeExamples.Add (label); + bar = new StatusBar () + { + Id = "statusBar", + X = Pos.Right (label), + Y = Pos.Top (label), + Width = Dim.Fill (), + }; + ConfigStatusBar (bar); + statusBarLikeExamples.Add (bar); + + foreach (FrameView frameView in Application.Top.Subviews.Where (f => f is FrameView)!) + { + foreach (Bar barView in frameView.Subviews.Where (b => b is Bar)!) + { + foreach (Shortcut sh in barView.Subviews.Where (s => s is Shortcut)!) + { + sh.Accept += (o, args) => + { + eventSource.Add ($"Accept: {sh!.SuperView.Id} {sh!.CommandView.Text}"); + eventLog.MoveDown (); + }; + } + } + } + } + + + //private void SetupContentMenu () + //{ + // Application.Top.Add (new Label { Text = "Right Click for Context Menu", X = Pos.Center (), Y = 4 }); + // Application.Top.MouseClick += ShowContextMenu; + //} + + //private void ShowContextMenu (object s, MouseEventEventArgs e) + //{ + // if (e.MouseEvent.Flags != MouseFlags.Button3Clicked) + // { + // return; + // } + + // var contextMenu = new Bar + // { + // Id = "contextMenu", + // X = e.MouseEvent.Position.X, + // Y = e.MouseEvent.Position.Y, + // Width = Dim.Auto (DimAutoStyle.Content), + // Height = Dim.Auto (DimAutoStyle.Content), + // Orientation = Orientation.Vertical, + // StatusBarStyle = false, + // BorderStyle = LineStyle.Rounded, + // Modal = true, + // }; + + // var newMenu = new Shortcut + // { + // Title = "_New...", + // Text = "Create a new file", + // Key = Key.N.WithCtrl, + // CanFocus = true + // }; + + // newMenu.Accept += (s, e) => + // { + // contextMenu.RequestStop (); + + // Application.AddTimeout ( + // new TimeSpan (0), + // () => + // { + // MessageBox.Query ("File", "New"); + + // return false; + // }); + // }; + + // var open = new Shortcut + // { + // Title = "_Open...", + // Text = "Show the File Open Dialog", + // Key = Key.O.WithCtrl, + // CanFocus = true + // }; + + // open.Accept += (s, e) => + // { + // contextMenu.RequestStop (); + + // Application.AddTimeout ( + // new TimeSpan (0), + // () => + // { + // MessageBox.Query ("File", "Open"); + + // return false; + // }); + // }; + + // var save = new Shortcut + // { + // Title = "_Save...", + // Text = "Save", + // Key = Key.S.WithCtrl, + // CanFocus = true + // }; + + // save.Accept += (s, e) => + // { + // contextMenu.RequestStop (); + + // Application.AddTimeout ( + // new TimeSpan (0), + // () => + // { + // MessageBox.Query ("File", "Save"); + + // return false; + // }); + // }; + + // var saveAs = new Shortcut + // { + // Title = "Save _As...", + // Text = "Save As", + // Key = Key.A.WithCtrl, + // CanFocus = true + // }; + + // saveAs.Accept += (s, e) => + // { + // contextMenu.RequestStop (); + + // Application.AddTimeout ( + // new TimeSpan (0), + // () => + // { + // MessageBox.Query ("File", "Save As"); + + // return false; + // }); + // }; + + // contextMenu.Add (newMenu, open, save, saveAs); + + // contextMenu.KeyBindings.Add (Key.Esc, Command.QuitToplevel); + + // contextMenu.Initialized += Menu_Initialized; + + // void Application_MouseEvent (object sender, MouseEvent e) + // { + // // If user clicks outside of the menuWindow, close it + // if (!contextMenu.Frame.Contains (e.Position.X, e.Position.Y)) + // { + // if (e.Flags is (MouseFlags.Button1Clicked or MouseFlags.Button3Clicked)) + // { + // contextMenu.RequestStop (); + // } + // } + // } + + // Application.MouseEvent += Application_MouseEvent; + + // Application.Run (contextMenu); + // contextMenu.Dispose (); + + // Application.MouseEvent -= Application_MouseEvent; + //} + + private void Menu_Initialized (object sender, EventArgs e) + { + // BUGBUG: this should not be needed + + ((View)(sender)).LayoutSubviews (); + } + + private void ConfigMenuBar (Bar bar) + { + var fileMenuBarItem = new Shortcut + { + Title = "_File", + HelpText = "File Menu", + Key = Key.D0.WithAlt, + }; + + var editMenuBarItem = new Shortcut + { + Title = "_Edit", + HelpText = "Edit Menu", + Key = Key.D1.WithAlt + }; + + var helpMenuBarItem = new Shortcut + { + Title = "_Help", + HelpText = "Halp Menu", + Key = Key.D2.WithAlt + }; + + bar.Add (fileMenuBarItem, editMenuBarItem, helpMenuBarItem); + } + + private void ConfigureMenu (Bar bar) + { + + var shortcut1 = new Shortcut + { + Title = "Z_igzag", + Key = Key.I.WithCtrl, + Text = "Gonna zig zag", + }; + + var shortcut2 = new Shortcut + { + Title = "Za_G", + Text = "Gonna zag", + Key = Key.G.WithAlt, + }; + + var line = new Line () + { + BorderStyle = LineStyle.Dotted, + Orientation = Orientation.Horizontal, + CanFocus = false, + }; + + var shortcut3 = new Shortcut + { + Title = "_Three", + Text = "The 3rd item", + Key = Key.D3.WithAlt, + }; + + bar.Add (shortcut1, shortcut2, line, shortcut3); + } + + private void ConfigStatusBar (Bar bar) + { + var shortcut = new Shortcut + { + Text = "Quit", + Title = "Q_uit", + Key = Key.Z.WithCtrl, + }; + + bar.Add (shortcut); + + shortcut = new Shortcut + { + Text = "Help Text", + Title = "Help", + Key = Key.F1, + }; + + bar.Add (shortcut); + + shortcut = new Shortcut + { + Title = "_Show/Hide", + Key = Key.F10, + CommandView = new CheckBox + { + CanFocus = false, + Text = "_Show/Hide" + }, + }; + + bar.Add (shortcut); + + var button1 = new Button + { + Text = "I'll Hide", + // Visible = false + }; + button1.Accept += Button_Clicked; + bar.Add (button1); + + shortcut.Accept += (s, e) => + { + button1.Visible = !button1.Visible; + button1.Enabled = button1.Visible; + e.Cancel = false; + }; + + bar.Add (new Label + { + HotKeySpecifier = new Rune ('_'), + Text = "Fo_cusLabel", + CanFocus = true + }); + + var button2 = new Button + { + Text = "Or me!", + }; + button2.Accept += (s, e) => Application.RequestStop (); + + bar.Add (button2); + + return; + + void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + + } + +} diff --git a/UICatalog/Scenarios/BasicColors.cs b/UICatalog/Scenarios/BasicColors.cs index b1b75c54f..b45cbec3c 100644 --- a/UICatalog/Scenarios/BasicColors.cs +++ b/UICatalog/Scenarios/BasicColors.cs @@ -111,5 +111,6 @@ public class BasicColors : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs index 370a5eb66..e880aa195 100644 --- a/UICatalog/Scenarios/Buttons.cs +++ b/UICatalog/Scenarios/Buttons.cs @@ -14,6 +14,8 @@ public class Buttons : Scenario { public override void Main () { + Application.Init (); + Window main = new () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" @@ -394,6 +396,7 @@ public class Buttons : Scenario main.Ready += (s, e) => radioGroup.Refresh (); Application.Run (main); main.Dispose (); + Application.Shutdown (); } /// diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index 02c11da5c..9387ed835 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -180,6 +180,7 @@ public class CharacterMap : Scenario Application.Run (top); top.Dispose (); + Application.Shutdown (); } private void _categoryList_Initialized (object sender, EventArgs e) { _charMap.Width = Dim.Fill () - _categoryList.Width; } diff --git a/UICatalog/Scenarios/ColorPicker.cs b/UICatalog/Scenarios/ColorPicker.cs index 5bc84b70d..9a76085c1 100644 --- a/UICatalog/Scenarios/ColorPicker.cs +++ b/UICatalog/Scenarios/ColorPicker.cs @@ -86,6 +86,7 @@ public class ColorPickers : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } /// Fired when background color is changed. diff --git a/UICatalog/Scenarios/ComputedLayout.cs b/UICatalog/Scenarios/ComputedLayout.cs index a28e866f8..dd5e4c376 100644 --- a/UICatalog/Scenarios/ComputedLayout.cs +++ b/UICatalog/Scenarios/ComputedLayout.cs @@ -53,6 +53,10 @@ public class ComputedLayout : Scenario app.LayoutComplete += (s, a) => { + if (horizontalRuler.Viewport.Width == 0 || horizontalRuler.Viewport.Height == 0) + { + return; + } horizontalRuler.Text = rule.Repeat ((int)Math.Ceiling (horizontalRuler.Viewport.Width / (double)rule.Length)) [ ..horizontalRuler.Viewport.Width]; @@ -409,5 +413,6 @@ public class ComputedLayout : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/ConfigurationEditor.cs b/UICatalog/Scenarios/ConfigurationEditor.cs index b24e35592..1e095a527 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -22,7 +22,7 @@ public class ConfigurationEditor : Scenario }; private static Action _editorColorSchemeChanged; - private StatusItem _lenStatusItem; + private Shortcut _lenShortcut; private TileView _tileView; [SerializableConfigurationProperty (Scope = typeof (AppScope))] @@ -41,6 +41,7 @@ public class ConfigurationEditor : Scenario Application.Init (); Toplevel top = new (); + _tileView = new TileView (0) { Width = Dim.Fill (), Height = Dim.Fill (1), Orientation = Orientation.Vertical, LineStyle = LineStyle.Single @@ -48,21 +49,33 @@ public class ConfigurationEditor : Scenario top.Add (_tileView); - _lenStatusItem = new StatusItem (KeyCode.CharMask, "Len: ", null); + _lenShortcut = new Shortcut () + { + Title = "Len: ", + }; - var statusBar = new StatusBar ( - new [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} Quit", - () => Quit () - ), - new (KeyCode.F5, "~F5~ Reload", () => Reload ()), - new (KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save ()), - _lenStatusItem - } - ); + var quitShortcut = new Shortcut () + { + Key = Application.QuitKey, + Title = $"{Application.QuitKey} Quit", + Action = Quit + }; + + var reloadShortcut = new Shortcut () + { + Key = Key.F5.WithShift, + Title = "Reload", + }; + reloadShortcut.Accept += (s, e) => { Reload (); }; + + var saveShortcut = new Shortcut () + { + Key = Key.F4, + Title = "Save", + Action = Save + }; + + var statusBar = new StatusBar ([quitShortcut, reloadShortcut, saveShortcut, _lenShortcut]); top.Add (statusBar); @@ -120,7 +133,7 @@ public class ConfigurationEditor : Scenario textView.Read (); - textView.Enter += (s, e) => { _lenStatusItem.Title = $"Len:{textView.Text.Length}"; }; + textView.Enter += (s, e) => { _lenShortcut.Title = $"Len:{textView.Text.Length}"; }; } Application.Top.LayoutSubviews (); diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 2584462bc..b3d734ad1 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; @@ -26,16 +25,23 @@ public class CsvEditor : Scenario private MenuItem _miCentered; private MenuItem _miLeft; private MenuItem _miRight; - private TextField _selectedCellLabel; + private TextField _selectedCellTextField; private TableView _tableView; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); - _tableView = new TableView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (1) }; + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new () + { + Title = $"{GetName ()}" + }; + + //appWindow.Height = Dim.Fill (1); // status bar + + _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (2) }; var fileMenu = new MenuBarItem ( "_File", @@ -53,97 +59,96 @@ public class CsvEditor : Scenario Menus = [ fileMenu, - new MenuBarItem ( - "_Edit", - new MenuItem [] - { - new ("_New Column", "", () => AddColumn ()), - new ("_New Row", "", () => AddRow ()), - new ( - "_Rename Column", - "", - () => RenameColumn () - ), - new ("_Delete Column", "", () => DeleteColum ()), - new ("_Move Column", "", () => MoveColumn ()), - new ("_Move Row", "", () => MoveRow ()), - new ("_Sort Asc", "", () => Sort (true)), - new ("_Sort Desc", "", () => Sort (false)) - } - ), - new MenuBarItem ( - "_View", - new [] - { - _miLeft = new MenuItem ( - "_Align Left", - "", - () => Align (Alignment.Start) - ), - _miRight = new MenuItem ( - "_Align Right", - "", - () => Align (Alignment.End) - ), - _miCentered = new MenuItem ( - "_Align Centered", - "", - () => Align (Alignment.Center) - ), + new ( + "_Edit", + new MenuItem [] + { + new ("_New Column", "", () => AddColumn ()), + new ("_New Row", "", () => AddRow ()), + new ( + "_Rename Column", + "", + () => RenameColumn () + ), + new ("_Delete Column", "", () => DeleteColum ()), + new ("_Move Column", "", () => MoveColumn ()), + new ("_Move Row", "", () => MoveRow ()), + new ("_Sort Asc", "", () => Sort (true)), + new ("_Sort Desc", "", () => Sort (false)) + } + ), + new ( + "_View", + new [] + { + _miLeft = new ( + "_Align Left", + "", + () => Align (Alignment.Start) + ), + _miRight = new ( + "_Align Right", + "", + () => Align (Alignment.End) + ), + _miCentered = new ( + "_Align Centered", + "", + () => Align (Alignment.Center) + ), - // Format requires hard typed data table, when we read a CSV everything is untyped (string) so this only works for new columns in this demo - _miCentered = new MenuItem ( - "_Set Format Pattern", - "", - () => SetFormat () - ) - } - ) + // Format requires hard typed data table, when we read a CSV everything is untyped (string) so this only works for new columns in this demo + _miCentered = new ( + "_Set Format Pattern", + "", + () => SetFormat () + ) + } + ) ] }; - Top.Add (menu); + appWindow.Add (menu); + + _selectedCellTextField = new () + { + Text = "0,0", + Width = 10, + Height = 1 + }; + _selectedCellTextField.TextChanged += SelectedCellLabel_TextChanged; var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - KeyCode.CtrlMask | KeyCode.O, - "~^O~ Open", - () => Open () - ), - new ( - KeyCode.CtrlMask | KeyCode.S, - "~^S~ Save", - () => Save () - ), - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) - } - ); - Top.Add (statusBar); - - Win.Add (_tableView); - - _selectedCellLabel = new TextField + [ + new (Application.QuitKey, "Quit", Quit, "Quit!"), + new (Key.O.WithCtrl, "Open", Open, "Open a file."), + new (Key.S.WithCtrl, "Save", Save, "Save current."), + new () + { + HelpText = "Cell:", + CommandView = _selectedCellTextField, + AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast, + Enabled = false + } + ]) { - X = 0, - Y = Pos.Bottom (_tableView), - Text = "0,0", - Width = Dim.Fill (), - TextAlignment = Alignment.End + AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; - _selectedCellLabel.TextChanged += SelectedCellLabel_TextChanged; + appWindow.Add (statusBar); - Win.Add (_selectedCellLabel); + appWindow.Add (_tableView); _tableView.SelectedCellChanged += OnSelectedCellChanged; _tableView.CellActivated += EditCurrentCell; _tableView.KeyDown += TableViewKeyPress; SetupScrollBar (); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } private void AddColumn () @@ -300,10 +305,10 @@ public class CsvEditor : Scenario var ok = new Button { Text = "Ok", IsDefault = true }; ok.Accept += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; + { + okPressed = true; + Application.RequestStop (); + }; var cancel = new Button { Text = "Cancel" }; cancel.Accept += (s, e) => { Application.RequestStop (); }; var d = new Dialog { Title = title, Buttons = [ok, cancel] }; @@ -425,9 +430,9 @@ public class CsvEditor : Scenario private void OnSelectedCellChanged (object sender, SelectedCellChangedEventArgs e) { // only update the text box if the user is not manually editing it - if (!_selectedCellLabel.HasFocus) + if (!_selectedCellTextField.HasFocus) { - _selectedCellLabel.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}"; + _selectedCellTextField.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}"; } if (_tableView.Table == null || _tableView.SelectedColumn == -1) @@ -446,7 +451,7 @@ public class CsvEditor : Scenario { var ofd = new FileDialog { - AllowedTypes = new List { new AllowedType ("Comma Separated Values", ".csv") } + AllowedTypes = new () { new AllowedType ("Comma Separated Values", ".csv") } }; ofd.Style.OkButtonText = "Open"; @@ -456,6 +461,7 @@ public class CsvEditor : Scenario { Open (ofd.Path); } + ofd.Dispose (); } @@ -496,7 +502,8 @@ public class CsvEditor : Scenario // Only set the current filename if we successfully loaded the entire file _currentFile = filename; - Win.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + _selectedCellTextField.SuperView.Enabled = true; + Application.Top.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; } catch (Exception ex) { @@ -561,13 +568,13 @@ public class CsvEditor : Scenario private void SelectedCellLabel_TextChanged (object sender, StateEventArgs e) { // if user is in the text control and editing the selected cell - if (!_selectedCellLabel.HasFocus) + if (!_selectedCellTextField.HasFocus) { return; } // change selected cell to the one the user has typed into the box - Match match = Regex.Match (_selectedCellLabel.Text, "^(\\d+),(\\d+)$"); + Match match = Regex.Match (_selectedCellTextField.Text, "^(\\d+),(\\d+)$"); if (match.Success) { diff --git a/UICatalog/Scenarios/DynamicStatusBar.cs b/UICatalog/Scenarios/DynamicStatusBar.cs index e4b0de705..3c93421ee 100644 --- a/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/UICatalog/Scenarios/DynamicStatusBar.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -12,15 +13,11 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Top Level Windows")] public class DynamicStatusBar : Scenario { - public override void Init () + public override void Main () { - Application.Init (); - Top = new (); - - Top.Add ( - new DynamicStatusBarSample { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" } - ); + Application.Run ().Dispose (); + Application.Shutdown (); } public class Binding @@ -90,9 +87,9 @@ public class DynamicStatusBar : Scenario public class DynamicStatusBarDetails : FrameView { - private StatusItem _statusItem; + private Shortcut _statusItem; - public DynamicStatusBarDetails (StatusItem statusItem = null) : this () + public DynamicStatusBarDetails (Shortcut statusItem = null) : this () { _statusItem = statusItem; Title = statusItem == null ? "Adding New StatusBar Item." : "Editing StatusBar Item."; @@ -155,7 +152,7 @@ public class DynamicStatusBar : Scenario bool CheckShortcut (KeyCode k, bool pre) { - StatusItem m = _statusItem != null ? _statusItem : new StatusItem (k, "", null); + Shortcut m = _statusItem != null ? _statusItem : new Shortcut (k, "", null); if (pre && !ShortcutHelper.PreShortcutValidation (k)) { @@ -166,28 +163,10 @@ public class DynamicStatusBar : Scenario if (!pre) { - if (!ShortcutHelper.PostShortcutValidation ( - ShortcutHelper.GetShortcutFromTag ( - TextShortcut.Text, - StatusBar.ShortcutDelimiter - ) - )) - { - TextShortcut.Text = ""; - - return false; - } - return true; } - TextShortcut.Text = - Key.ToString ( - k, - StatusBar - .ShortcutDelimiter - ); //ShortcutHelper.GetShortcutTag (k, StatusBar.ShortcutDelimiter); - + TextShortcut.Text = k.ToString (); return true; } @@ -213,7 +192,7 @@ public class DynamicStatusBar : Scenario public TextField TextTitle { get; } public Action CreateAction (DynamicStatusItem item) { return () => MessageBox.ErrorQuery (item.Title, item.Action, "Ok"); } - public void EditStatusItem (StatusItem statusItem) + public void EditStatusItem (Shortcut statusItem) { if (statusItem == null) { @@ -231,12 +210,7 @@ public class DynamicStatusBar : Scenario ? GetTargetAction (statusItem.Action) : string.Empty; - TextShortcut.Text = - Key.ToString ( - (KeyCode)statusItem.Shortcut, - StatusBar - .ShortcutDelimiter - ); //ShortcutHelper.GetShortcutTag (statusItem.Shortcut, StatusBar.ShortcutDelimiter) ?? ""; + TextShortcut.Text = statusItem.CommandView.Text; } public DynamicStatusItem EnterStatusItem () @@ -334,31 +308,16 @@ public class DynamicStatusBar : Scenario public class DynamicStatusBarSample : Window { private readonly ListView _lstItems; - private StatusItem _currentEditStatusItem; + private Shortcut _currentEditStatusItem; private int _currentSelectedStatusBar = -1; - private StatusItem _currentStatusItem; + private Shortcut _currentStatusItem; private StatusBar _statusBar; public DynamicStatusBarSample () { DataContext = new DynamicStatusItemModel (); - var _frmDelimiter = new FrameView - { - X = Pos.Center (), - Y = 0, - Width = 25, - Height = 4, - Title = "Shortcut Delimiter:" - }; - - var _txtDelimiter = new TextField { X = Pos.Center (), Width = 2, Text = $"{StatusBar.ShortcutDelimiter}" }; - - _txtDelimiter.TextChanged += (s, _) => - StatusBar.ShortcutDelimiter = _txtDelimiter.Text.ToRunes () [0]; - _frmDelimiter.Add (_txtDelimiter); - - Add (_frmDelimiter); + Title = $"{Application.QuitKey} to Quit"; var _frmStatusBar = new FrameView { @@ -404,18 +363,18 @@ public class DynamicStatusBar : Scenario Y = Pos.Top (_frmStatusBar), Width = Dim.Fill (), Height = Dim.Fill (4), - Title = "StatusBar Item Details:" + Title = "Shortcut Details:" }; Add (_frmStatusBarDetails); _btnUp.Accept += (s, e) => { int i = _lstItems.SelectedItem; - StatusItem statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; + Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null; if (statusItem != null) { - StatusItem [] items = _statusBar.Items; + Shortcut [] items = _statusBar.Subviews.Cast ().ToArray (); if (i > 0) { @@ -434,11 +393,11 @@ public class DynamicStatusBar : Scenario _btnDown.Accept += (s, e) => { int i = _lstItems.SelectedItem; - StatusItem statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; + Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].Shortcut : null; if (statusItem != null) { - StatusItem [] items = _statusBar.Items; + Shortcut [] items = _statusBar.Subviews.Cast ().ToArray (); if (i < items.Length - 1) { @@ -511,9 +470,9 @@ public class DynamicStatusBar : Scenario return; } - StatusItem newStatusItem = CreateNewStatusBar (item); + Shortcut newStatusItem = CreateNewStatusBar (item); _currentSelectedStatusBar++; - _statusBar.AddItemAt (_currentSelectedStatusBar, newStatusItem); + _statusBar.AddShortcutAt (_currentSelectedStatusBar, newStatusItem); DataContext.Items.Add (new DynamicStatusItemList (newStatusItem.Title, newStatusItem)); _lstItems.MoveDown (); SetFrameDetails (); @@ -521,13 +480,13 @@ public class DynamicStatusBar : Scenario _btnRemove.Accept += (s, e) => { - StatusItem statusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].StatusItem + Shortcut statusItem = DataContext.Items.Count > 0 + ? DataContext.Items [_lstItems.SelectedItem].Shortcut : null; if (statusItem != null) { - _statusBar.RemoveItem (_currentSelectedStatusBar); + _statusBar.RemoveShortcut (_currentSelectedStatusBar); DataContext.Items.RemoveAt (_lstItems.SelectedItem); if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1) @@ -542,8 +501,8 @@ public class DynamicStatusBar : Scenario _lstItems.Enter += (s, e) => { - StatusItem statusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].StatusItem + Shortcut statusItem = DataContext.Items.Count > 0 + ? DataContext.Items [_lstItems.SelectedItem].Shortcut : null; SetFrameDetails (statusItem); }; @@ -582,14 +541,14 @@ public class DynamicStatusBar : Scenario var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter); - void SetFrameDetails (StatusItem statusItem = null) + void SetFrameDetails (Shortcut statusItem = null) { - StatusItem newStatusItem; + Shortcut newStatusItem; if (statusItem == null) { newStatusItem = DataContext.Items.Count > 0 - ? DataContext.Items [_lstItems.SelectedItem].StatusItem + ? DataContext.Items [_lstItems.SelectedItem].Shortcut : null; } else @@ -608,10 +567,10 @@ public class DynamicStatusBar : Scenario } } - void SetListViewSource (StatusItem _currentStatusItem, bool fill = false) + void SetListViewSource (Shortcut _currentStatusItem, bool fill = false) { DataContext.Items = []; - StatusItem statusItem = _currentStatusItem; + Shortcut statusItem = _currentStatusItem; if (!fill) { @@ -620,35 +579,28 @@ public class DynamicStatusBar : Scenario if (statusItem != null) { - foreach (StatusItem si in _statusBar.Items) + foreach (Shortcut si in _statusBar.Subviews.Cast ()) { DataContext.Items.Add (new DynamicStatusItemList (si.Title, si)); } } } - StatusItem CreateNewStatusBar (DynamicStatusItem item) + Shortcut CreateNewStatusBar (DynamicStatusItem item) { - var newStatusItem = new StatusItem ( - ShortcutHelper.GetShortcutFromTag ( - item.Shortcut, - StatusBar.ShortcutDelimiter - ), - item.Title, - _frmStatusBarDetails.CreateAction (item) - ); + var newStatusItem = new Shortcut (Key.Empty, item.Title, null); return newStatusItem; } void UpdateStatusItem ( - StatusItem _currentEditStatusItem, + Shortcut _currentEditStatusItem, DynamicStatusItem statusItem, int index ) { _currentEditStatusItem = CreateNewStatusBar (statusItem); - _statusBar.Items [index] = _currentEditStatusItem; + //_statusBar.Items [index] = _currentEditStatusItem; if (DataContext.Items.Count == 0) { @@ -702,15 +654,15 @@ public class DynamicStatusBar : Scenario { public DynamicStatusItemList () { } - public DynamicStatusItemList (string title, StatusItem statusItem) + public DynamicStatusItemList (string title, Shortcut statusItem) { Title = title; - StatusItem = statusItem; + Shortcut = statusItem; } - public StatusItem StatusItem { get; set; } + public Shortcut Shortcut { get; set; } public string Title { get; set; } - public override string ToString () { return $"{Title}, {StatusItem}"; } + public override string ToString () { return $"{Title}, {Shortcut}"; } } public class DynamicStatusItemModel : INotifyPropertyChanged diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 05277e7d4..73b14ffd0 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -238,27 +238,22 @@ public class Editor : Scenario _appWindow.Add (menu); - var siCursorPosition = new StatusItem (KeyCode.Null, "", null); + var siCursorPosition = new Shortcut(KeyCode.Null, "", null); var statusBar = new StatusBar ( new [] { + new (Application.QuitKey, $"Quit", Quit), + new (Key.F2, "Open", Open), + new (Key.F3, "Save", () => Save ()), + new (Key.F4, "Save As", () => SaveAs ()), + new (Key.Empty, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null), siCursorPosition, - new (KeyCode.F2, "~F2~ Open", () => Open ()), - new (KeyCode.F3, "~F3~ Save", () => Save ()), - new (KeyCode.F4, "~F4~ Save As", () => SaveAs ()), - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ), - new ( - KeyCode.Null, - $"OS Clipboard IsSupported : {Clipboard.IsSupported}", - null - ) } - ); + ) + { + AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast + }; _textView.UnwrappedCursorPosition += (s, e) => { diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index b60a95c3b..754a8aa10 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using Terminal.Gui; +using Application = Terminal.Gui.Application; namespace UICatalog.Scenarios; @@ -18,12 +20,12 @@ public class GraphViewExample : Scenario private GraphView _graphView; private MenuItem _miDiags; private MenuItem _miShowBorder; + private ViewDiagnosticFlags _viewDiagnostics; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + Application.Init (); + Toplevel app = new (); _graphs = new [] { @@ -41,155 +43,177 @@ public class GraphViewExample : Scenario { Menus = [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ( - "Scatter _Plot", - "", - () => _graphs [_currentGraph = - 0] () - ), - new ( - "_V Bar Graph", - "", - () => _graphs [_currentGraph = - 1] () - ), - new ( - "_H Bar Graph", - "", - () => _graphs [_currentGraph = - 2] () - ), - new ( - "P_opulation Pyramid", - "", - () => _graphs [_currentGraph = - 3] () - ), - new ( - "_Line Graph", - "", - () => _graphs [_currentGraph = - 4] () - ), - new ( - "Sine _Wave", - "", - () => _graphs [_currentGraph = - 5] () - ), - new ( - "Silent _Disco", - "", - () => _graphs [_currentGraph = - 6] () - ), - new ( - "_Multi Bar Graph", - "", - () => _graphs [_currentGraph = - 7] () - ), - new ("_Quit", "", () => Quit ()) - } - ), - new MenuBarItem ( - "_View", - new [] - { - new ("Zoom _In", "", () => Zoom (0.5f)), - new ("Zoom _Out", "", () => Zoom (2f)), - new ("MarginLeft++", "", () => Margin (true, true)), - new ("MarginLeft--", "", () => Margin (true, false)), - new ("MarginBottom++", "", () => Margin (false, true)), - new ("MarginBottom--", "", () => Margin (false, false)), - _miShowBorder = new MenuItem ( - "_Enable Margin, Border, and Padding", - "", - () => ShowBorder () - ) - { - Checked = true, - CheckType = MenuItemCheckStyle - .Checked - }, - _miDiags = new MenuItem ( - "Dri_ver Diagnostics", - "", - () => EnableDiagnostics () - ) - { - Checked = View.Diagnostics - == (ViewDiagnosticFlags - .Padding - | ViewDiagnosticFlags - .Ruler), - CheckType = MenuItemCheckStyle.Checked - } - } - ) + new ( + "_File", + new MenuItem [] + { + new ( + "Scatter _Plot", + "", + () => _graphs [_currentGraph = + 0] () + ), + new ( + "_V Bar Graph", + "", + () => _graphs [_currentGraph = + 1] () + ), + new ( + "_H Bar Graph", + "", + () => _graphs [_currentGraph = + 2] () + ), + new ( + "P_opulation Pyramid", + "", + () => _graphs [_currentGraph = + 3] () + ), + new ( + "_Line Graph", + "", + () => _graphs [_currentGraph = + 4] () + ), + new ( + "Sine _Wave", + "", + () => _graphs [_currentGraph = + 5] () + ), + new ( + "Silent _Disco", + "", + () => _graphs [_currentGraph = + 6] () + ), + new ( + "_Multi Bar Graph", + "", + () => _graphs [_currentGraph = + 7] () + ), + new ("_Quit", "", () => Quit ()) + } + ), + new ( + "_View", + new [] + { + new ("Zoom _In", "", () => Zoom (0.5f)), + new ("Zoom _Out", "", () => Zoom (2f)), + new ("MarginLeft++", "", () => Margin (true, true)), + new ("MarginLeft--", "", () => Margin (true, false)), + new ("MarginBottom++", "", () => Margin (false, true)), + new ("MarginBottom--", "", () => Margin (false, false)), + _miShowBorder = new ( + "_Enable Margin, Border, and Padding", + "", + () => ShowBorder () + ) + { + Checked = true, + CheckType = MenuItemCheckStyle + .Checked + }, + _miDiags = new ( + "_Diagnostics", + "", + () => ToggleDiagnostics () + ) + { + Checked = View.Diagnostics + == (ViewDiagnosticFlags + .Padding + | ViewDiagnosticFlags + .Ruler), + CheckType = MenuItemCheckStyle.Checked + } + } + ) ] }; - Top.Add (menu); + app.Add (menu); - _graphView = new GraphView + _graphView = new() { X = 0, - Y = 0, + Y = 1, Width = Dim.Percent (70), - Height = Dim.Fill (), + Height = Dim.Fill (1), BorderStyle = LineStyle.Single }; _graphView.Border.Thickness = _thickness; _graphView.Margin.Thickness = _thickness; _graphView.Padding.Thickness = _thickness; - Win.Add (_graphView); + app.Add (_graphView); var frameRight = new FrameView { - X = Pos.Right (_graphView) + 1, - Y = 0, + X = Pos.Right (_graphView), + Y = Pos.Top (_graphView), Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Height (_graphView), Title = "About" }; frameRight.Add ( - _about = new TextView { Width = Dim.Fill (), Height = Dim.Fill () } + _about = new() { Width = Dim.Fill (), Height = Dim.Fill () } ); - Win.Add (frameRight); + app.Add (frameRight); var statusBar = new StatusBar ( - new StatusItem [] + new Shortcut [] { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ), - new ( - KeyCode.CtrlMask | KeyCode.G, - "~^G~ Next", - () => _graphs [_currentGraph++ % _graphs.Length] () - ) + new (Key.G.WithCtrl, "Next Graph", () => _graphs [_currentGraph++ % _graphs.Length] ()), + new (Key.CursorUp, "Zoom In", () => Zoom (0.5f)), + new (Key.CursorDown, "Zoom Out", () => Zoom (2f)) } ); - Top.Add (statusBar); + app.Add (statusBar); + + var diagShortcut = new Shortcut + { + Key = Key.F10, + CommandView = new CheckBox + { + Title = "Diagnostics", + CanFocus = false + } + }; + statusBar.Add (diagShortcut).Accept += DiagShortcut_Accept; + + _graphs [_currentGraph++ % _graphs.Length] (); + + _viewDiagnostics = View.Diagnostics; + Application.Run (app); + View.Diagnostics = _viewDiagnostics; + app.Dispose (); + Application.Shutdown (); } - private void EnableDiagnostics () + private void DiagShortcut_Accept (object sender, CancelEventArgs e) + { + ToggleDiagnostics (); + + if (sender is Shortcut shortcut && shortcut.CommandView is CheckBox checkBox) + { + checkBox.Checked = _miDiags.Checked; + } + } + + private void ToggleDiagnostics () { _miDiags.Checked = !_miDiags.Checked; View.Diagnostics = _miDiags.Checked == true - ? ViewDiagnosticFlags.Padding - | ViewDiagnosticFlags.Ruler - : ViewDiagnosticFlags.Off; + ? ViewDiagnosticFlags.Padding + | ViewDiagnosticFlags.Ruler + : ViewDiagnosticFlags.Off; Application.Refresh (); } @@ -216,7 +240,7 @@ public class GraphViewExample : Scenario _about.Text = "Housing Expenditures by income thirds 1996-2003"; Color fore = _graphView.ColorScheme.Normal.Foreground == new Color (ColorName.Black) - ? new Color (ColorName.White) + ? new (ColorName.White) : _graphView.ColorScheme.Normal.Foreground; var black = new Attribute (fore, Color.Black); var cyan = new Attribute (Color.BrightCyan, Color.Black); @@ -238,7 +262,7 @@ public class GraphViewExample : Scenario series.AddBars ("'02", stiple, 6600, 11000, 16700); series.AddBars ("'03", stiple, 7000, 12000, 17000); - _graphView.CellSize = new PointF (0.25f, 1000); + _graphView.CellSize = new (0.25f, 1000); _graphView.Series.Add (series); _graphView.SetNeedsDisplay (); @@ -254,20 +278,20 @@ public class GraphViewExample : Scenario _graphView.AxisY.Minimum = 0; - var legend = new LegendAnnotation (new Rectangle (_graphView.Viewport.Width - 20, 0, 20, 5)); + var legend = new LegendAnnotation (new (_graphView.Viewport.Width - 20, 0, 20, 5)); legend.AddEntry ( - new GraphCellToRender (stiple, series.SubSeries.ElementAt (0).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (0).OverrideBarColor), "Lower Third" ); legend.AddEntry ( - new GraphCellToRender (stiple, series.SubSeries.ElementAt (1).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (1).OverrideBarColor), "Middle Third" ); legend.AddEntry ( - new GraphCellToRender (stiple, series.SubSeries.ElementAt (2).OverrideBarColor), + new (stiple, series.SubSeries.ElementAt (2).OverrideBarColor), "Upper Third" ); _graphView.Annotations.Add (legend); @@ -299,7 +323,7 @@ public class GraphViewExample : Scenario for (var i = 0; i < 31; i++) { bars.Add ( - new BarSeriesBar (null, stiple, r.Next (0, 100)) + new (null, stiple, r.Next (0, 100)) { //ColorGetter = colorDelegate } @@ -319,7 +343,7 @@ public class GraphViewExample : Scenario _graphView.Series.Add (series); // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (1, 10); + _graphView.CellSize = new (1, 10); _graphView.AxisX.Increment = 0; // No graph ticks _graphView.AxisX.ShowLabelsEvery = 0; // no labels @@ -374,7 +398,7 @@ public class GraphViewExample : Scenario var barSeries = new BarSeries { - Bars = new List + Bars = new() { new ("Switzerland", softStiple, 83.4f), new ( @@ -451,7 +475,7 @@ public class GraphViewExample : Scenario barSeries.Orientation = Orientation.Vertical; // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (0.1f, 0.25f); + _graphView.CellSize = new (0.1f, 0.25f); // No axis marks since Bar will add it's own categorical marks _graphView.AxisX.Increment = 0f; @@ -469,14 +493,14 @@ public class GraphViewExample : Scenario _graphView.MarginLeft = 6; // Start the graph at 80 years because that is where most of our data is - _graphView.ScrollOffset = new PointF (0, 80); + _graphView.ScrollOffset = new (0, 80); } else { barSeries.Orientation = Orientation.Horizontal; // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (0.1f, 1f); + _graphView.CellSize = new (0.1f, 1f); // No axis marks since Bar will add it's own categorical marks _graphView.AxisY.Increment = 0f; @@ -495,7 +519,7 @@ public class GraphViewExample : Scenario _graphView.MarginLeft = (uint)barSeries.Bars.Max (b => b.Text.Length) + 2; // Start the graph at 80 years because that is where most of our data is - _graphView.ScrollOffset = new PointF (80, 0); + _graphView.ScrollOffset = new (80, 0); } _graphView.SetNeedsDisplay (); @@ -522,7 +546,7 @@ public class GraphViewExample : Scenario for (var i = 0; i < 10; i++) { - randomPoints.Add (new PointF (r.Next (100), r.Next (100))); + randomPoints.Add (new (r.Next (100), r.Next (100))); } var points = new ScatterSeries { Points = randomPoints }; @@ -535,14 +559,14 @@ public class GraphViewExample : Scenario _graphView.Series.Add (points); _graphView.Annotations.Add (line); - randomPoints = new List (); + randomPoints = new (); for (var i = 0; i < 10; i++) { - randomPoints.Add (new PointF (r.Next (100), r.Next (100))); + randomPoints.Add (new (r.Next (100), r.Next (100))); } - var points2 = new ScatterSeries { Points = randomPoints, Fill = new GraphCellToRender ((Rune)'x', red) }; + var points2 = new ScatterSeries { Points = randomPoints, Fill = new ((Rune)'x', red) }; var line2 = new PathAnnotation { @@ -553,7 +577,7 @@ public class GraphViewExample : Scenario _graphView.Annotations.Add (line2); // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (2, 5); + _graphView.CellSize = new (2, 5); // leave space for axis labels _graphView.MarginBottom = 2; @@ -574,10 +598,10 @@ public class GraphViewExample : Scenario new TextAnnotation { Text = "(Max)", - GraphPosition = new PointF ( - max.X + 2 * _graphView.CellSize.X, - max.Y - ) + GraphPosition = new ( + max.X + 2 * _graphView.CellSize.X, + max.Y + ) } ); @@ -597,7 +621,7 @@ public class GraphViewExample : Scenario _graphView.Series.Add ( new ScatterSeries { - Points = new List + Points = new() { new (1, 1.007f), new (2, 4.002f), @@ -719,7 +743,7 @@ public class GraphViewExample : Scenario ); // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (1, 5); + _graphView.CellSize = new (1, 5); // leave space for axis labels _graphView.MarginBottom = 2; @@ -772,10 +796,10 @@ public class GraphViewExample : Scenario _graphView.Reset (); // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (100_000, 1); + _graphView.CellSize = new (100_000, 1); //center the x axis in middle of screen to show both sides - _graphView.ScrollOffset = new PointF (-3_000_000, 0); + _graphView.ScrollOffset = new (-3_000_000, 0); _graphView.AxisX.Text = "Number Of People"; _graphView.AxisX.Increment = 500_000; @@ -801,7 +825,7 @@ public class GraphViewExample : Scenario var malesSeries = new BarSeries { Orientation = Orientation.Horizontal, - Bars = new List + Bars = new() { new ("0-4", stiple, -2009363), new ("5-9", stiple, -2108550), @@ -832,7 +856,7 @@ public class GraphViewExample : Scenario var femalesSeries = new BarSeries { Orientation = Orientation.Horizontal, - Bars = new List + Bars = new() { new ("0-4", stiple, 1915127), new ("5-9", stiple, 2011016), @@ -895,15 +919,15 @@ public class GraphViewExample : Scenario // Generate line graph with 2,000 points for (float x = -500; x < 500; x += 0.5f) { - points.Points.Add (new PointF (x, (float)Math.Sin (x))); - line.Points.Add (new PointF (x, (float)Math.Sin (x))); + points.Points.Add (new (x, (float)Math.Sin (x))); + line.Points.Add (new (x, (float)Math.Sin (x))); } _graphView.Series.Add (points); _graphView.Annotations.Add (line); // How much graph space each cell of the console depicts - _graphView.CellSize = new PointF (0.1f, 0.1f); + _graphView.CellSize = new (0.1f, 0.1f); // leave space for axis labels _graphView.MarginBottom = 2; @@ -920,7 +944,7 @@ public class GraphViewExample : Scenario _graphView.AxisY.Text = "↑Y"; _graphView.AxisY.LabelGetter = v => v.Value.ToString ("N2"); - _graphView.ScrollOffset = new PointF (-2.5f, -1); + _graphView.ScrollOffset = new (-2.5f, -1); _graphView.SetNeedsDisplay (); } @@ -946,10 +970,10 @@ public class GraphViewExample : Scenario private void Zoom (float factor) { - _graphView.CellSize = new PointF ( - _graphView.CellSize.X * factor, - _graphView.CellSize.Y * factor - ); + _graphView.CellSize = new ( + _graphView.CellSize.X * factor, + _graphView.CellSize.Y * factor + ); _graphView.AxisX.Increment *= factor; _graphView.AxisY.Increment *= factor; @@ -967,11 +991,11 @@ public class GraphViewExample : Scenario public DiscoBarSeries () { - _green = new Attribute (Color.BrightGreen, Color.Black); - _brightgreen = new Attribute (Color.Green, Color.Black); - _brightyellow = new Attribute (Color.BrightYellow, Color.Black); - _red = new Attribute (Color.Red, Color.Black); - _brightred = new Attribute (Color.BrightRed, Color.Black); + _green = new (Color.BrightGreen, Color.Black); + _brightgreen = new (Color.Green, Color.Black); + _brightyellow = new (Color.BrightYellow, Color.Black); + _red = new (Color.Red, Color.Black); + _brightred = new (Color.BrightRed, Color.Black); } protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn) diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index 1427f61c0..b941378c6 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -16,21 +16,31 @@ public class HexEditor : Scenario private HexView _hexView; private MenuItem _miAllowEdits; private bool _saved = true; - private StatusItem _siPositionChanged; + private Shortcut _siPositionChanged; private StatusBar _statusBar; - public override void Setup () + public override void Main () { - Win.Title = GetName () + "-" + _fileName ?? "Untitled"; + Application.Init (); + Toplevel app = new Toplevel () + { + ColorScheme = Colors.ColorSchemes ["Base"] + }; CreateDemoFile (_fileName); - //CreateUnicodeDemoFile (_fileName); - - _hexView = new HexView (LoadFile ()) { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; + _hexView = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("Demo text."))) + { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill (1), + Title = _fileName ?? "Untitled", + BorderStyle = LineStyle.Rounded, + }; _hexView.Edited += _hexView_Edited; _hexView.PositionChanged += _hexView_PositionChanged; - Win.Add (_hexView); + app.Add (_hexView); var menu = new MenuBar { @@ -74,20 +84,20 @@ public class HexEditor : Scenario ) ] }; - Top.Add (menu); + app.Add (menu); _statusBar = new StatusBar ( new [] { - new (KeyCode.F2, "~F2~ Open", () => Open ()), - new (KeyCode.F3, "~F3~ Save", () => Save ()), + new (Key.F2, "Open", () => Open ()), + new (Key.F3, "Save", () => Save ()), new ( Application.QuitKey, - $"{Application.QuitKey} to Quit", + $"Quit", () => Quit () ), - _siPositionChanged = new StatusItem ( - KeyCode.Null, + _siPositionChanged = new Shortcut ( + Key.Empty, $"Position: { _hexView.Position } Line: { @@ -100,8 +110,17 @@ public class HexEditor : Scenario () => { } ) } - ); - Top.Add (_statusBar); + ) + { + AlignmentModes = AlignmentModes.IgnoreFirstOrLast + }; + app.Add (_statusBar); + + _hexView.Source = LoadFile (); + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); } private void _hexView_Edited (object sender, HexViewEditEventArgs e) { _saved = false; } @@ -109,16 +128,7 @@ public class HexEditor : Scenario private void _hexView_PositionChanged (object sender, HexViewEventArgs obj) { _siPositionChanged.Title = - $"Position: { - obj.Position - } Line: { - obj.CursorPosition.Y - } Col: { - obj.CursorPosition.X - } Line length: { - obj.BytesPerLine - }"; - _statusBar.SetNeedsDisplay (); + $"Position: {obj.Position} Line: {obj.CursorPosition.Y} Col: {obj.CursorPosition.X} Line length: {obj.BytesPerLine}"; } private void Copy () { MessageBox.ErrorQuery ("Not Implemented", "Functionality not yet implemented.", "Ok"); } @@ -154,7 +164,7 @@ public class HexEditor : Scenario { var stream = new MemoryStream (); - if (!_saved && _hexView != null && _hexView.Edits.Count > 0) + if (!_saved && _hexView.Edits.Count > 0) { if (MessageBox.ErrorQuery ( "Save", @@ -175,12 +185,12 @@ public class HexEditor : Scenario { byte [] bin = File.ReadAllBytes (_fileName); stream.Write (bin); - Win.Title = GetName () + "-" + _fileName; + _hexView.Title = _fileName; _saved = true; } else { - Win.Title = GetName () + "-" + (_fileName ?? "Untitled"); + _hexView.Title = (_fileName ?? "Untitled"); } return stream; diff --git a/UICatalog/Scenarios/HotKeys.cs b/UICatalog/Scenarios/HotKeys.cs index 926d743ca..fb50813fc 100644 --- a/UICatalog/Scenarios/HotKeys.cs +++ b/UICatalog/Scenarios/HotKeys.cs @@ -123,5 +123,6 @@ public class HotKeys : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/InteractiveTree.cs b/UICatalog/Scenarios/InteractiveTree.cs index fafc34351..1480969f9 100644 --- a/UICatalog/Scenarios/InteractiveTree.cs +++ b/UICatalog/Scenarios/InteractiveTree.cs @@ -10,52 +10,48 @@ public class InteractiveTree : Scenario { private TreeView _treeView; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + Application.Init (); + var appWindow = new Toplevel () + { + Title = GetName (), + }; var menu = new MenuBar { Menus = [ - new MenuBarItem ("_File", new MenuItem [] { new ("_Quit", "", Quit) }) + new ("_File", new MenuItem [] { new ("_Quit", "", Quit) }) ] }; - Top.Add (menu); + appWindow.Add (menu); - _treeView = new TreeView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (1) }; + _treeView = new () + { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill (1) + }; _treeView.KeyDown += TreeView_KeyPress; - Win.Add (_treeView); + appWindow.Add (_treeView); var statusBar = new StatusBar ( - new StatusItem [] + new Shortcut [] { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - Quit - ), - new ( - KeyCode.CtrlMask | KeyCode.C, - "~^C~ Add Child", - AddChildNode - ), - new ( - KeyCode.CtrlMask | KeyCode.T, - "~^T~ Add Root", - AddRootNode - ), - new ( - KeyCode.CtrlMask | KeyCode.R, - "~^R~ Rename Node", - RenameNode - ) + new (Application.QuitKey, "Quit", Quit), + new (Key.C.WithCtrl, "Add Child", AddChildNode), + new (Key.T.WithCtrl, "Add Root", AddRootNode), + new (Key.R.WithCtrl, "Rename Node", RenameNode) } ); - Top.Add (statusBar); + appWindow.Add (statusBar); + + Application.Run (appWindow); + appWindow.Dispose (); + Application.Shutdown (); } private void AddChildNode () @@ -87,10 +83,10 @@ public class InteractiveTree : Scenario var ok = new Button { Text = "Ok", IsDefault = true }; ok.Accept += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; + { + okPressed = true; + Application.RequestStop (); + }; var cancel = new Button { Text = "Cancel" }; cancel.Accept += (s, e) => Application.RequestStop (); var d = new Dialog { Title = title, Buttons = [ok, cancel] }; @@ -128,7 +124,7 @@ public class InteractiveTree : Scenario private void TreeView_KeyPress (object sender, Key obj) { - if (obj.KeyCode == KeyCode.Delete) + if (obj.KeyCode == Key.Delete) { ITreeNode toDelete = _treeView.SelectedObject; diff --git a/UICatalog/Scenarios/LineCanvasExperiment.cs b/UICatalog/Scenarios/LineCanvasExperiment.cs index 2d097b317..8061e3e5d 100644 --- a/UICatalog/Scenarios/LineCanvasExperiment.cs +++ b/UICatalog/Scenarios/LineCanvasExperiment.cs @@ -139,5 +139,6 @@ public class LineCanvasExperiment : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/LineViewExample.cs b/UICatalog/Scenarios/LineViewExample.cs index dc386ba94..f846562c6 100644 --- a/UICatalog/Scenarios/LineViewExample.cs +++ b/UICatalog/Scenarios/LineViewExample.cs @@ -9,60 +9,60 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Borders")] public class LineViewExample : Scenario { - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + Application.Init (); + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); var menu = new MenuBar { Menus = [ - new MenuBarItem ("_File", new MenuItem [] { new ("_Quit", "", () => Quit ()) }) + new ("_File", new MenuItem [] { new ("_Quit", "", () => Quit ()) }) ] }; - Top.Add (menu); + appWindow.Add (menu); - Win.Add (new Label { Y = 0, Text = "Regular Line" }); + appWindow.Add (new Label { Y = 1, Text = "Regular Line" }); // creates a horizontal line - var line = new LineView { Y = 1 }; + var line = new LineView { Y = 2 }; - Win.Add (line); + appWindow.Add (line); - Win.Add (new Label { Y = 2, Text = "Double Width Line" }); + appWindow.Add (new Label { Y = 3, Text = "Double Width Line" }); // creates a horizontal line - var doubleLine = new LineView { Y = 3, LineRune = (Rune)'\u2550' }; + var doubleLine = new LineView { Y = 4, LineRune = (Rune)'\u2550' }; - Win.Add (doubleLine); + appWindow.Add (doubleLine); - Win.Add (new Label { Y = 4, Text = "Short Line" }); + appWindow.Add (new Label { Y = 5, Text = "Short Line" }); // creates a horizontal line var shortLine = new LineView { Y = 5, Width = 10 }; - Win.Add (shortLine); + appWindow.Add (shortLine); - Win.Add (new Label { Y = 6, Text = "Arrow Line" }); + appWindow.Add (new Label { Y = 7, Text = "Arrow Line" }); // creates a horizontal line var arrowLine = new LineView { - Y = 7, Width = 10, StartingAnchor = CM.Glyphs.LeftTee, EndingAnchor = (Rune)'>' + Y = 8, Width = 10, StartingAnchor = CM.Glyphs.LeftTee, EndingAnchor = (Rune)'>' }; - Win.Add (arrowLine); + appWindow.Add (arrowLine); - Win.Add (new Label { Y = 9, X = 11, Text = "Vertical Line" }); + appWindow.Add (new Label { Y = 10, X = 11, Text = "Vertical Line" }); // creates a horizontal line var verticalLine = new LineView (Orientation.Vertical) { X = 25 }; - Win.Add (verticalLine); + appWindow.Add (verticalLine); - Win.Add (new Label { Y = 11, X = 28, Text = "Vertical Arrow" }); + appWindow.Add (new Label { Y = 12, X = 28, Text = "Vertical Arrow" }); // creates a horizontal line var verticalArrow = new LineView (Orientation.Vertical) @@ -70,19 +70,22 @@ public class LineViewExample : Scenario X = 27, StartingAnchor = CM.Glyphs.TopTee, EndingAnchor = (Rune)'V' }; - Win.Add (verticalArrow); + appWindow.Add (verticalArrow); var statusBar = new StatusBar ( - new StatusItem [] + new Shortcut [] { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) + new (Application.QuitKey, "Quit", Quit) } ); - Top.Add (statusBar); + appWindow.Add (statusBar); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } private void Quit () { Application.RequestStop (); } diff --git a/UICatalog/Scenarios/ListColumns.cs b/UICatalog/Scenarios/ListColumns.cs index 09aad302e..f0cd740ef 100644 --- a/UICatalog/Scenarios/ListColumns.cs +++ b/UICatalog/Scenarios/ListColumns.cs @@ -47,19 +47,24 @@ public class ListColumns : Scenario return list; } - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); - _listColView = new() + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new () + { + Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" + }; + + _listColView = new () { X = 0, - Y = 0, + Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), - Style = new() + Style = new () { ShowHeaders = false, ShowHorizontalHeaderOverline = false, @@ -209,36 +214,20 @@ public class ListColumns : Scenario ] }; - Top.Add (menu); + appWindow.Add (menu); var statusBar = new StatusBar ( - new StatusItem [] + new Shortcut [] { - new ( - KeyCode.F2, - "~F2~ OpenBigListEx", - () => OpenSimpleList (true) - ), - new ( - KeyCode.F3, - "~F3~ CloseExample", - () => CloseExample () - ), - new ( - KeyCode.F4, - "~F4~ OpenSmListEx", - () => OpenSimpleList (false) - ), - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) + new (Key.F2, "OpenBigListEx", () => OpenSimpleList (true)), + new (Key.F3, "CloseExample", CloseExample), + new (Key.F4, "OpenSmListEx", () => OpenSimpleList (false)), + new (Application.QuitKey, "Quit", Quit) } ); - Top.Add (statusBar); + appWindow.Add (statusBar); - Win.Add (_listColView); + appWindow.Add (_listColView); var selectedCellLabel = new Label { @@ -250,18 +239,18 @@ public class ListColumns : Scenario TextAlignment = Alignment.End }; - Win.Add (selectedCellLabel); + appWindow.Add (selectedCellLabel); _listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_listColView.SelectedRow},{_listColView.SelectedColumn}"; }; _listColView.KeyDown += TableViewKeyPress; SetupScrollBar (); - _alternatingColorScheme = new() + _alternatingColorScheme = new () { - Disabled = Win.ColorScheme.Disabled, - HotFocus = Win.ColorScheme.HotFocus, - Focus = Win.ColorScheme.Focus, + Disabled = appWindow.ColorScheme.Disabled, + HotFocus = appWindow.ColorScheme.HotFocus, + Focus = appWindow.ColorScheme.Focus, Normal = new (Color.White, Color.BrightBlue) }; @@ -269,6 +258,13 @@ public class ListColumns : Scenario _listColView.MouseClick += (s, e) => { _listColView.ScreenToCell (e.MouseEvent.Position, out int? clickedCol); }; _listColView.KeyBindings.Add (Key.Space, Command.Accept); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } private void CloseExample () { _listColView.Table = null; } @@ -370,7 +366,7 @@ public class ListColumns : Scenario private void TableViewKeyPress (object sender, Key e) { - if (e.KeyCode == KeyCode.Delete) + if (e.KeyCode == Key.Delete) { // set all selected cells to null foreach (Point pt in _listColView.GetAllSelectedCells ()) diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index 83ce63d4a..a11d6950d 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -207,6 +207,7 @@ public class ListViewWithSelection : Scenario public event NotifyCollectionChangedEventHandler CollectionChanged; public int Count => Scenarios != null ? Scenarios.Count : 0; public int Length { get; private set; } + public bool SuspendCollectionChangedEvent { get => throw new System.NotImplementedException (); set => throw new System.NotImplementedException (); } public void Render ( ListView container, diff --git a/UICatalog/Scenarios/Mouse.cs b/UICatalog/Scenarios/Mouse.cs index f6ad84a51..84c256247 100644 --- a/UICatalog/Scenarios/Mouse.cs +++ b/UICatalog/Scenarios/Mouse.cs @@ -18,7 +18,7 @@ public class Mouse : Scenario Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }; - Slider filterSlider = new() + Slider filterSlider = new () { Title = "_Filter", X = 0, @@ -57,7 +57,7 @@ public class Mouse : Scenario win.Add (clearButton); Label ml; var count = 0; - ml = new() { X = Pos.Right (filterSlider), Y = 0, Text = "Mouse: " }; + ml = new () { X = Pos.Right (filterSlider), Y = 0, Text = "Mouse: " }; win.Add (ml); @@ -138,7 +138,7 @@ public class Mouse : Scenario } }; - label = new() + label = new () { Text = "_Window Events:", X = Pos.Right (appLog) + 1, @@ -184,6 +184,7 @@ public class Mouse : Scenario Application.Run (win); win.Dispose (); + Application.Shutdown (); } public class MouseDemo : View diff --git a/UICatalog/Scenarios/MultiColouredTable.cs b/UICatalog/Scenarios/MultiColouredTable.cs index 7050c0639..4ac82bb00 100644 --- a/UICatalog/Scenarios/MultiColouredTable.cs +++ b/UICatalog/Scenarios/MultiColouredTable.cs @@ -14,36 +14,33 @@ public class MultiColouredTable : Scenario private DataTable _table; private TableViewColors _tableView; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); - _tableView = new TableViewColors { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (1) }; + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new () + { + Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" + }; + + _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; var menu = new MenuBar { Menus = [ - new MenuBarItem ("_File", new MenuItem [] { new ("_Quit", "", () => Quit ()) }) + new ("_File", new MenuItem [] { new ("_Quit", "", Quit) }) ] }; - Top.Add (menu); + appWindow.Add (menu); - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) - } - ); - Top.Add (statusBar); + var statusBar = new StatusBar (new Shortcut [] { new (Application.QuitKey, "Quit", Quit) }); - Win.Add (_tableView); + appWindow.Add (statusBar); + + appWindow.Add (_tableView); _tableView.CellActivated += EditCurrentCell; @@ -58,15 +55,22 @@ public class MultiColouredTable : Scenario dt.Rows.Add (DBNull.Value, DBNull.Value); dt.Rows.Add (DBNull.Value, DBNull.Value); - _tableView.ColorScheme = new ColorScheme + _tableView.ColorScheme = new () { - Disabled = Win.ColorScheme.Disabled, - HotFocus = Win.ColorScheme.HotFocus, - Focus = Win.ColorScheme.Focus, - Normal = new Attribute (Color.DarkGray, Color.Black) + Disabled = appWindow.ColorScheme.Disabled, + HotFocus = appWindow.ColorScheme.HotFocus, + Focus = appWindow.ColorScheme.Focus, + Normal = new (Color.DarkGray, Color.Black) }; _tableView.Table = new DataTableSource (_table = dt); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } private void EditCurrentCell (object sender, CellActivatedEventArgs e) @@ -101,10 +105,10 @@ public class MultiColouredTable : Scenario var ok = new Button { Text = "Ok", IsDefault = true }; ok.Accept += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; + { + okPressed = true; + Application.RequestStop (); + }; var cancel = new Button { Text = "Cancel" }; cancel.Accept += (s, e) => { Application.RequestStop (); }; var d = new Dialog { Title = title, Buttons = [ok, cancel] }; @@ -137,7 +141,7 @@ public class MultiColouredTable : Scenario { if (unicorns != -1 && i >= unicorns && i <= unicorns + 8) { - Driver.SetAttribute (new Attribute (Color.White, cellColor.Background)); + Driver.SetAttribute (new (Color.White, cellColor.Background)); } if (rainbows != -1 && i >= rainbows && i <= rainbows + 8) @@ -147,60 +151,60 @@ public class MultiColouredTable : Scenario switch (letterOfWord) { case 0: - Driver.SetAttribute (new Attribute (Color.Red, cellColor.Background)); + Driver.SetAttribute (new (Color.Red, cellColor.Background)); break; case 1: Driver.SetAttribute ( - new Attribute ( - Color.BrightRed, - cellColor.Background - ) + new ( + Color.BrightRed, + cellColor.Background + ) ); break; case 2: Driver.SetAttribute ( - new Attribute ( - Color.BrightYellow, - cellColor.Background - ) + new ( + Color.BrightYellow, + cellColor.Background + ) ); break; case 3: - Driver.SetAttribute (new Attribute (Color.Green, cellColor.Background)); + Driver.SetAttribute (new (Color.Green, cellColor.Background)); break; case 4: Driver.SetAttribute ( - new Attribute ( - Color.BrightGreen, - cellColor.Background - ) + new ( + Color.BrightGreen, + cellColor.Background + ) ); break; case 5: Driver.SetAttribute ( - new Attribute ( - Color.BrightBlue, - cellColor.Background - ) + new ( + Color.BrightBlue, + cellColor.Background + ) ); break; case 6: Driver.SetAttribute ( - new Attribute ( - Color.BrightCyan, - cellColor.Background - ) + new ( + Color.BrightCyan, + cellColor.Background + ) ); break; case 7: - Driver.SetAttribute (new Attribute (Color.Cyan, cellColor.Background)); + Driver.SetAttribute (new (Color.Cyan, cellColor.Background)); break; } diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 9a2613ae6..dd962bfc1 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -11,7 +11,7 @@ namespace UICatalog.Scenarios; public class Notepad : Scenario { private TabView _focusedTabView; - private StatusItem _lenStatusItem; + public Shortcut LenShortcut { get; private set; } private int _numNewTabs = 1; private TabView _tabView; @@ -39,11 +39,11 @@ public class Notepad : Scenario | KeyCode.CtrlMask | KeyCode.AltMask ), - new ("_Open", "", () => Open ()), - new ("_Save", "", () => Save ()), + new ("_Open", "", Open), + new ("_Save", "", Save), new ("Save _As", "", () => SaveAs ()), - new ("_Close", "", () => Close ()), - new ("_Quit", "", () => Quit ()) + new ("_Close", "", Close), + new ("_Quit", "", Quit) } ), new ( @@ -66,33 +66,31 @@ public class Notepad : Scenario split.LineStyle = LineStyle.None; top.Add (split); + LenShortcut = new (Key.Empty, "Len: ", null); - _lenStatusItem = new (KeyCode.CharMask, "Len: ", null); - - var statusBar = new StatusBar ( - new [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ), - - // These shortcut keys don't seem to work correctly in linux - //new StatusItem(Key.CtrlMask | Key.N, "~^O~ Open", () => Open()), - //new StatusItem(Key.CtrlMask | Key.N, "~^N~ New", () => New()), - - new (KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save ()), - new (KeyCode.CtrlMask | KeyCode.W, "~^W~ Close", () => Close ()), - _lenStatusItem + var statusBar = new StatusBar (new [] { + new (Application.QuitKey, $"Quit", Quit), + new Shortcut(Key.F2, "Open", Open), + new Shortcut(Key.F1, "New", New), + new (Key.F3, "Save", Save), + new (Key.F6, "Close", Close), + LenShortcut } - ); + ) + { + AlignmentModes = AlignmentModes.IgnoreFirstOrLast + }; + top.Add (statusBar); + _focusedTabView = _tabView; _tabView.SelectedTabChanged += TabView_SelectedTabChanged; _tabView.Enter += (s, e) => _focusedTabView = _tabView; - top.Add (statusBar); - top.Ready += (s, e) => New (); + top.Ready += (s, e) => + { + New (); + LenShortcut.Title = $"Len:{_focusedTabView.Text?.Length ?? 0}"; + }; Application.Run (top); top.Dispose (); @@ -279,7 +277,7 @@ public class Notepad : Scenario /// File that was read or null if a new blank document private void Open (FileInfo fileInfo, string tabName) { - var tab = new OpenedFile { DisplayText = tabName, File = fileInfo }; + var tab = new OpenedFile (this) { DisplayText = tabName, File = fileInfo }; tab.View = tab.CreateTextView (fileInfo); tab.SavedText = tab.View.Text; tab.RegisterTextViewEvents (_focusedTabView); @@ -323,7 +321,8 @@ public class Notepad : Scenario private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) { - _lenStatusItem.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}"; + e.NewTab?.View?.SetFocus (); } @@ -370,11 +369,13 @@ public class Notepad : Scenario e.MouseEvent.Handled = true; } - private class OpenedFile : Tab + private class OpenedFile (Notepad notepad) : Tab { + private Notepad _notepad = notepad; + public OpenedFile CloneTo (TabView other) { - var newTab = new OpenedFile { DisplayText = base.Text, File = File }; + var newTab = new OpenedFile (_notepad) { DisplayText = base.Text, File = File }; newTab.View = newTab.CreateTextView (newTab.File); newTab.SavedText = newTab.View.Text; newTab.RegisterTextViewEvents (other); @@ -410,28 +411,27 @@ public class Notepad : Scenario var textView = (TextView)View; // when user makes changes rename tab to indicate unsaved - textView.KeyUp += (s, k) => - { - // if current text doesn't match saved text - bool areDiff = UnsavedChanges; + textView.ContentsChanged += (s, k) => + { + // if current text doesn't match saved text + bool areDiff = UnsavedChanges; - if (areDiff) - { - if (!Text.EndsWith ('*')) - { - Text = Text + '*'; - parent.SetNeedsDisplay (); - } - } - else - { - if (Text.EndsWith ('*')) - { - Text = Text.TrimEnd ('*'); - parent.SetNeedsDisplay (); - } - } - }; + if (areDiff) + { + if (!DisplayText.EndsWith ('*')) + { + DisplayText = Text + '*'; + } + } + else + { + if (DisplayText.EndsWith ('*')) + { + DisplayText = Text.TrimEnd ('*'); + } + } + _notepad.LenShortcut.Title = $"Len:{textView.Text.Length}"; + }; } /// The text of the tab the last time it was saved @@ -452,7 +452,7 @@ public class Notepad : Scenario System.IO.File.WriteAllText (File.FullName, newText); SavedText = newText; - Text = Text.TrimEnd ('*'); + DisplayText = DisplayText.TrimEnd ('*'); } } } diff --git a/UICatalog/Scenarios/RunTExample.cs b/UICatalog/Scenarios/RunTExample.cs index ce82b1863..4dcb69030 100644 --- a/UICatalog/Scenarios/RunTExample.cs +++ b/UICatalog/Scenarios/RunTExample.cs @@ -6,16 +6,13 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Top Level Windows")] public class RunTExample : Scenario { - public override void Init () + public override void Main () { // No need to call Init if Application.Run is used - Application.Run (); - - Application.Top.Dispose (); + Application.Run ().Dispose (); + Application.Shutdown (); } - public override void Run () { } - public class ExampleWindow : Window { private readonly TextField _usernameText; @@ -27,7 +24,7 @@ public class RunTExample : Scenario // Create input components and labels var usernameLabel = new Label { Text = "Username:" }; - _usernameText = new TextField + _usernameText = new() { // Position text field adjacent to the label X = Pos.Right (usernameLabel) + 1, @@ -64,21 +61,21 @@ public class RunTExample : Scenario // When login button is clicked display a message popup btnLogin.Accept += (s, e) => - { - if (_usernameText.Text == "admin" && passwordText.Text == "password") - { - MessageBox.Query ("Login Successful", $"Username: {_usernameText.Text}", "Ok"); - Application.RequestStop (); - } - else - { - MessageBox.ErrorQuery ( - "Error Logging In", - "Incorrect username or password (hint: admin/password)", - "Ok" - ); - } - }; + { + if (_usernameText.Text == "admin" && passwordText.Text == "password") + { + MessageBox.Query ("Login Successful", $"Username: {_usernameText.Text}", "Ok"); + Application.RequestStop (); + } + else + { + MessageBox.ErrorQuery ( + "Error Logging In", + "Incorrect username or password (hint: admin/password)", + "Ok" + ); + } + }; // Add the views to the Window Add (usernameLabel, _usernameText, passwordLabel, passwordText, btnLogin); diff --git a/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index 217d8838e..97c15c5a9 100644 --- a/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -17,11 +17,11 @@ public class RuneWidthGreaterThanOne : Scenario private TextField _text; private Window _win; - public override void Init () + public override void Main () { Application.Init (); - Top = new (); + Toplevel topLevel = new (); var menu = new MenuBar { @@ -87,16 +87,17 @@ public class RuneWidthGreaterThanOne : Scenario }; _win = new Window { X = 5, Y = 5, Width = Dim.Fill (22), Height = Dim.Fill (5) }; _win.Add (_label, _text, _button, _labelR, _labelV); - Top.Add (menu, _win); + topLevel.Add (menu, _win); WideRunes (); //NarrowRunes (); //MixedRunes (); - Application.Run (Top); + Application.Run (topLevel); + topLevel.Dispose (); + Application.Shutdown (); } - public override void Run () { } private void MixedMessage (object sender, EventArgs e) { MessageBox.Query ("Say Hello 你", $"Hello {_text.Text}", "Ok"); } private void MixedRunes () diff --git a/UICatalog/Scenarios/Scrolling.cs b/UICatalog/Scenarios/Scrolling.cs index c9af1da10..581b2693e 100644 --- a/UICatalog/Scenarios/Scrolling.cs +++ b/UICatalog/Scenarios/Scrolling.cs @@ -247,6 +247,7 @@ public class Scrolling : Scenario app.Loaded -= App_Loaded; app.Unloaded -= app_Unloaded; app.Dispose (); + Application.Shutdown (); return; diff --git a/UICatalog/Scenarios/Shortcuts.cs b/UICatalog/Scenarios/Shortcuts.cs new file mode 100644 index 000000000..515ae96c9 --- /dev/null +++ b/UICatalog/Scenarios/Shortcuts.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Timers; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("Shortcuts", "Illustrates Shortcut class.")] +[ScenarioCategory ("Controls")] +public class Shortcuts : Scenario +{ + public override void Main () + { + Application.Init (); + Window app = new (); + + app.Loaded += App_Loaded; + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); + } + + // Setting everything up in Loaded handler because we change the + // QuitKey and it only sticks if changed after init + private void App_Loaded (object sender, EventArgs e) + { + Application.QuitKey = Key.Z.WithCtrl; + Application.Top.Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"; + + ObservableCollection eventSource = new (); + + var eventLog = new ListView + { + X = Pos.AnchorEnd (), + Width = 40, + Height = Dim.Fill (4), + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Source = new ListWrapper (eventSource), + BorderStyle = LineStyle.Double, + Title = "E_vents" + }; + Application.Top.Add (eventLog); + + var vShortcut1 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Width = 35, + Title = "A_pp Shortcut", + Key = Key.F1, + Text = "Width is 35", + KeyBindingScope = KeyBindingScope.Application, + }; + Application.Top.Add (vShortcut1); + + var vShortcut2 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut1), + Width = 35, + Key = Key.F2, + Text = "Width is 35", + KeyBindingScope = KeyBindingScope.HotKey, + CommandView = new RadioGroup + { + Orientation = Orientation.Vertical, + RadioLabels = ["O_ne", "T_wo", "Th_ree", "Fo_ur"] + } + }; + + ((RadioGroup)vShortcut2.CommandView).SelectedItemChanged += (o, args) => + { + eventSource.Add ($"SelectedItemChanged: {o.GetType ().Name} - {args.SelectedItem}"); + eventLog.MoveDown (); + }; + + vShortcut2.Accept += (o, args) => + { + // Cycle to next item. If at end, set 0 + if (((RadioGroup)vShortcut2.CommandView).SelectedItem < ((RadioGroup)vShortcut2.CommandView).RadioLabels.Length - 1) + { + ((RadioGroup)vShortcut2.CommandView).SelectedItem++; + } + else + { + ((RadioGroup)vShortcut2.CommandView).SelectedItem = 0; + } + }; + Application.Top.Add (vShortcut2); + + var vShortcut3 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut2), + CommandView = new CheckBox { Text = "_Align" }, + Key = Key.F5.WithCtrl.WithAlt.WithShift, + HelpText = "Width is Fill", + Width = Dim.Fill () - Dim.Width (eventLog), + KeyBindingScope = KeyBindingScope.HotKey, + }; + + ((CheckBox)vShortcut3.CommandView).Toggled += (s, e) => + { + if (vShortcut3.CommandView is CheckBox cb) + { + eventSource.Add ($"Toggled: {cb.Text}"); + eventLog.MoveDown (); + + var max = 0; + var toAlign = Application.Top.Subviews.Where (v => v is Shortcut { Orientation: Orientation.Vertical, Width: not DimAbsolute }); + + if (e.NewValue == true) + { + foreach (Shortcut peer in toAlign) + { + // DANGER: KeyView is internal so we can't access it. So we assume this is how it works. + max = Math.Max (max, peer.Key.ToString ().GetColumns ()); + } + } + + foreach (Shortcut peer in toAlign) + { + peer.MinimumKeyTextSize = max; + } + } + }; + Application.Top.Add (vShortcut3); + + var vShortcut4 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut3), + Width = Dim.Width (vShortcut3), + CommandView = new Button + { + Title = "B_utton", + }, + HelpText = "Width is Fill", + Key = Key.K, + KeyBindingScope = KeyBindingScope.HotKey, + }; + Button button = (Button)vShortcut4.CommandView; + vShortcut4.CommandView.Accept += Button_Clicked; + + Application.Top.Add (vShortcut4); + + var vShortcut5 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut4), + Width = Dim.Width (vShortcut4), + + Key = Key.F4, + HelpText = "CommandView.CanFocus", + KeyBindingScope = KeyBindingScope.HotKey, + CommandView = new CheckBox { Text = "_CanFocus" }, + }; + + ((CheckBox)vShortcut5.CommandView).Toggled += (s, e) => + { + if (vShortcut5.CommandView is CheckBox cb) + { + eventSource.Add ($"Toggled: {cb.Text}"); + eventLog.MoveDown (); + + foreach (Shortcut peer in Application.Top.Subviews.Where (v => v is Shortcut)!) + { + if (peer.CanFocus) + { + peer.CommandView.CanFocus = e.NewValue == true; + } + } + } + }; + Application.Top.Add (vShortcut5); + + var vShortcutSlider = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut5), + HelpText = "Width is Fill", + Width = Dim.Width (vShortcut5), + + KeyBindingScope = KeyBindingScope.HotKey, + CommandView = new Slider + { + Orientation = Orientation.Vertical, + AllowEmpty = true + }, + Key = Key.F5, + }; + + ((Slider)vShortcutSlider.CommandView).Options = new () { new () { Legend = "A" }, new () { Legend = "B" }, new () { Legend = "C" } }; + ((Slider)vShortcutSlider.CommandView).SetOption (0); + + ((Slider)vShortcutSlider.CommandView).OptionsChanged += (o, args) => + { + eventSource.Add ($"OptionsChanged: {o.GetType ().Name} - {string.Join (",", ((Slider)o).GetSetOptions ())}"); + eventLog.MoveDown (); + }; + + Application.Top.Add (vShortcutSlider); + + var vShortcut6 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcutSlider), + Width = Dim.Width (vShortcutSlider), + + Title = "_No Key", + HelpText = "Keyless", + }; + + Application.Top.Add (vShortcut6); + + + var vShortcut7 = new Shortcut + { + Orientation = Orientation.Vertical, + X = 0, + Y = Pos.Bottom (vShortcut6), + Width = Dim.Width (vShortcutSlider), + Key = Key.F6, + Title = "Not _very much help", + HelpText = "", + }; + + Application.Top.Add (vShortcut7); + vShortcut7.SetFocus (); + + + // Horizontal + var hShortcut1 = new Shortcut + { + X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1), + Y = Pos.Bottom (eventLog) + 1, + Key = Key.F7, + HelpText = "Horizontal", + CanFocus = false + }; + + hShortcut1.CommandView = new ProgressBar + { + Text = "Progress", + Title = "P", + Fraction = 0.5f, + Width = 10, + Height = 1, + ProgressBarStyle = ProgressBarStyle.Continuous + }; + hShortcut1.CommandView.Width = 10; + hShortcut1.CommandView.Height = 1; + hShortcut1.CommandView.CanFocus = false; + + Timer timer = new (10) + { + AutoReset = true, + }; + timer.Elapsed += (o, args) => + { + if (hShortcut1.CommandView is ProgressBar pb) + { + if (pb.Fraction == 1.0) + { + pb.Fraction = 0; + } + pb.Fraction += 0.01f; + + Application.Wakeup (); + + pb.SetNeedsDisplay (); + } + }; + timer.Start (); + + Application.Top.Add (hShortcut1); + + var textField = new TextField () + { + Text = "Edit me", + Width = 10, + Height = 1, + CanFocus = true + }; + + var hShortcut2 = new Shortcut + { + Orientation = Orientation.Horizontal, + X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1), + Y = Pos.Top (hShortcut1), + Key = Key.F8, + HelpText = "TextField", + CanFocus = true, + CommandView = textField, + }; + + Application.Top.Add (hShortcut2); + + var hShortcutBG = new Shortcut + { + Orientation = Orientation.Horizontal, + X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1) - 1, + Y = Pos.Top (hShortcut2), + Key = Key.F9, + HelpText = "BG Color", + CanFocus = false + }; + + var bgColor = new ColorPicker () + { + CanFocus = false, + BoxHeight = 1, + BoxWidth = 1, + }; + bgColor.ColorChanged += (o, args) => + { + Application.Top.ColorScheme = new ColorScheme (Application.Top.ColorScheme) + { + Normal = new Attribute (Application.Top.ColorScheme.Normal.Foreground, args.Color), + }; + }; + hShortcutBG.CommandView = bgColor; + + Application.Top.Add (hShortcutBG); + + var hShortcut3 = new Shortcut + { + Orientation = Orientation.Horizontal, + X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1), + Y = Pos.Top (hShortcut2), + Key = Key.Esc, + KeyBindingScope = KeyBindingScope.Application, + Title = "Quit", + HelpText = "App Scope", + CanFocus = false + }; + hShortcut3.Accept += (o, args) => + { + Application.RequestStop (); + }; + + Application.Top.Add (hShortcut3); + + foreach (View sh in Application.Top.Subviews.Where (v => v is Shortcut)!) + { + if (sh is Shortcut shortcut) + { + shortcut.Accept += (o, args) => + { + eventSource.Add ($"Accept: {shortcut!.CommandView.Text}"); + eventLog.MoveDown (); + args.Cancel = true; + }; + } + } + + //((CheckBox)vShortcut5.CommandView).OnToggled (); + } + + private void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } +} diff --git a/UICatalog/Scenarios/SingleBackgroundWorker.cs b/UICatalog/Scenarios/SingleBackgroundWorker.cs index 37904201e..896cac096 100644 --- a/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -12,15 +12,12 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Top Level Windows")] public class SingleBackgroundWorker : Scenario { - public override void Init () + public override void Main () { - Application.Run (); - - Application.Top.Dispose (); + Application.Run ().Dispose (); + Application.Shutdown (); } - public override void Run () { } - public class MainApp : Toplevel { private readonly ListView _listLog; @@ -34,57 +31,47 @@ public class SingleBackgroundWorker : Scenario { Menus = [ - new MenuBarItem ( - "_Options", - new MenuItem [] - { - new ( - "_Run Worker", - "", - () => RunWorker (), - null, - null, - KeyCode.CtrlMask | KeyCode.R - ), - null, - new ( - "_Quit", - "", - () => Application.RequestStop (), - null, - null, - KeyCode.CtrlMask | KeyCode.Q - ) - } - ) + new ( + "_Options", + new MenuItem [] + { + new ( + "_Run Worker", + "", + () => RunWorker (), + null, + null, + KeyCode.CtrlMask | KeyCode.R + ), + null, + new ( + "_Quit", + "", + () => Application.RequestStop (), + null, + null, + KeyCode.CtrlMask | KeyCode.Q + ) + } + ) ] }; Add (menu); var statusBar = new StatusBar ( - new [] - { - new StatusItem ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Application.RequestStop () - ), - new StatusItem ( - KeyCode.CtrlMask | KeyCode.P, - "~^R~ Run Worker", - () => RunWorker () - ) - } - ); + [ + new (Application.QuitKey, "Quit", () => Application.RequestStop ()), + new (Key.R.WithCtrl, "Run Worker", RunWorker) + ]); Add (statusBar); - var workerLogTop = new Toplevel () { Title = "Worker Log Top"}; + var workerLogTop = new Toplevel { Title = "Worker Log Top" }; workerLogTop.Add ( - new Label { X = Pos.Center (), Y = 0, Text = "Worker Log" } - ); + new Label { X = Pos.Center (), Y = 0, Text = "Worker Log" } + ); - _listLog = new ListView + _listLog = new() { X = 0, Y = 2, @@ -99,26 +86,26 @@ public class SingleBackgroundWorker : Scenario private void RunWorker () { - _worker = new BackgroundWorker { WorkerSupportsCancellation = true }; + _worker = new() { WorkerSupportsCancellation = true }; var cancel = new Button { Text = "Cancel Worker" }; cancel.Accept += (s, e) => - { - if (_worker == null) - { - _log.Add ($"Worker is not running at {DateTime.Now}!"); - _listLog.SetNeedsDisplay (); + { + if (_worker == null) + { + _log.Add ($"Worker is not running at {DateTime.Now}!"); + _listLog.SetNeedsDisplay (); - return; - } + return; + } - _log.Add ( - $"Worker {_startStaging}.{_startStaging:fff} is canceling at {DateTime.Now}!" - ); - _listLog.SetNeedsDisplay (); - _worker.CancelAsync (); - }; + _log.Add ( + $"Worker {_startStaging}.{_startStaging:fff} is canceling at {DateTime.Now}!" + ); + _listLog.SetNeedsDisplay (); + _worker.CancelAsync (); + }; _startStaging = DateTime.Now; _log.Add ($"Worker is started at {_startStaging}.{_startStaging:fff}"); @@ -164,15 +151,7 @@ public class SingleBackgroundWorker : Scenario { // Failed _log.Add ( - $"Exception occurred { - e.Error.Message - } on Worker { - _startStaging - }.{ - _startStaging - :fff} at { - DateTime.Now - }" + $"Exception occurred {e.Error.Message} on Worker {_startStaging}.{_startStaging:fff} at {DateTime.Now}" ); _listLog.SetNeedsDisplay (); } @@ -195,7 +174,7 @@ public class SingleBackgroundWorker : Scenario var builderUI = new StagingUIController (_startStaging, e.Result as ObservableCollection); - var top = Application.Top; + Toplevel top = Application.Top; top.Visible = false; Application.Current.Visible = false; builderUI.Load (); @@ -217,7 +196,7 @@ public class SingleBackgroundWorker : Scenario public StagingUIController (DateTime? start, ObservableCollection list) { - _top = new Toplevel + _top = new() { Title = "_top", Width = Dim.Fill (), Height = Dim.Fill () }; @@ -250,46 +229,44 @@ public class SingleBackgroundWorker : Scenario { Menus = [ - new MenuBarItem ( - "_Stage", - new MenuItem [] - { - new ( - "_Close", - "", - () => - { - if (Close ()) - { - Application.RequestStop (); - } - }, - null, - null, - KeyCode.CtrlMask | KeyCode.C - ) - } - ) + new ( + "_Stage", + new MenuItem [] + { + new ( + "_Close", + "", + () => + { + if (Close ()) + { + Application.RequestStop (); + } + }, + null, + null, + KeyCode.CtrlMask | KeyCode.C + ) + } + ) ] }; _top.Add (menu); var statusBar = new StatusBar ( - new [] - { - new StatusItem ( - KeyCode.CtrlMask | KeyCode.C, - "~^C~ Close", - () => - { - if (Close ()) - { - Application.RequestStop (); - } - } - ) - } - ); + [ + new ( + Key.C.WithCtrl, + "Close", + () => + { + if (Close ()) + { + Application.RequestStop (); + } + } + ) + ]); _top.Add (statusBar); Title = $"Worker started at {start}.{start:fff}"; @@ -309,18 +286,11 @@ public class SingleBackgroundWorker : Scenario _top.Add (this); } - public void Load () { + public void Load () + { Application.Run (_top); _top.Dispose (); _top = null; } - - ///// - //protected override void Dispose (bool disposing) - //{ - // _top?.Dispose (); - // _top = null; - // base.Dispose (disposing); - //} } } diff --git a/UICatalog/Scenarios/Sliders.cs b/UICatalog/Scenarios/Sliders.cs index fc4c1a9d5..6b52cb52e 100644 --- a/UICatalog/Scenarios/Sliders.cs +++ b/UICatalog/Scenarios/Sliders.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Tracing; using System.Linq; using System.Text; using Terminal.Gui; @@ -242,7 +244,7 @@ public class Sliders : Scenario } }; configView.Add (dimAutoUsesMin); - + #region Slider Orientation Slider Slider orientationSlider = new (new List { "Horizontal", "Vertical" }) @@ -393,7 +395,7 @@ public class Sliders : Scenario FrameView spacingOptions = new () { Title = "Spacing Options", - X = Pos.Right(orientationSlider), + X = Pos.Right (orientationSlider), Y = Pos.Top (orientationSlider), Width = Dim.Fill (), Height = Dim.Auto (), @@ -407,7 +409,7 @@ public class Sliders : Scenario Buttons.NumericUpDown innerSpacingUpDown = new () { - X = Pos.Right(label) + 1 + X = Pos.Right (label) + 1 }; innerSpacingUpDown.Value = app.Subviews.OfType ().First ().MinimumInnerSpacing; @@ -429,8 +431,8 @@ public class Sliders : Scenario - spacingOptions.Add(label, innerSpacingUpDown); - configView.Add(spacingOptions); + spacingOptions.Add (label, innerSpacingUpDown); + configView.Add (spacingOptions); #endregion @@ -564,6 +566,35 @@ public class Sliders : Scenario #endregion Config Slider + ObservableCollection eventSource = new (); + var eventLog = new ListView + { + X = Pos.Right (sliderBGColor), + Y = Pos.Bottom (spacingOptions), + Width = Dim.Fill (), + Height = Dim.Fill (), + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Source = new ListWrapper (eventSource) + }; + configView.Add (eventLog); + + + foreach (Slider slider in app.Subviews.Where (v => v is Slider)!) + { + slider.Accept += (o, args) => + { + eventSource.Add ($"Accept: {string.Join(",", slider.GetSetOptions ())}"); + eventLog.MoveDown (); + args.Cancel = true; + }; + slider.OptionsChanged += (o, args) => + { + eventSource.Add ($"OptionsChanged: {string.Join (",", slider.GetSetOptions ())}"); + eventLog.MoveDown (); + args.Cancel = true; + }; + } + app.FocusFirst (); Application.Run (app); diff --git a/UICatalog/Scenarios/SyntaxHighlighting.cs b/UICatalog/Scenarios/SyntaxHighlighting.cs index 2aa49d4b2..ce4e970a2 100644 --- a/UICatalog/Scenarios/SyntaxHighlighting.cs +++ b/UICatalog/Scenarios/SyntaxHighlighting.cs @@ -120,70 +120,77 @@ public class SyntaxHighlighting : Scenario } } - public override void Setup () + public override void Main () { - Win.Title = GetName (); + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); var menu = new MenuBar { Menus = [ - new MenuBarItem ( - "_TextView", - new [] - { - _miWrap = new MenuItem ( - "_Word Wrap", - "", - () => WordWrap () - ) - { - CheckType = MenuItemCheckStyle - .Checked - }, - null, - new ( - "_Syntax Highlighting", - "", - () => ApplySyntaxHighlighting () - ), - null, - new ( - "_Load Rune Cells", - "", - () => ApplyLoadRuneCells () - ), - new ( - "_Save Rune Cells", - "", - () => SaveRuneCells () - ), - null, - new ("_Quit", "", () => Quit ()) - } - ) + new ( + "_TextView", + new [] + { + _miWrap = new ( + "_Word Wrap", + "", + () => WordWrap () + ) + { + CheckType = MenuItemCheckStyle + .Checked + }, + null, + new ( + "_Syntax Highlighting", + "", + () => ApplySyntaxHighlighting () + ), + null, + new ( + "_Load Rune Cells", + "", + () => ApplyLoadRuneCells () + ), + new ( + "_Save Rune Cells", + "", + () => SaveRuneCells () + ), + null, + new ("_Quit", "", () => Quit ()) + } + ) ] }; - Top.Add (menu); + appWindow.Add (menu); - _textView = new TextView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; + _textView = new() + { + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill (1) + }; ApplySyntaxHighlighting (); - Win.Add (_textView); + appWindow.Add (_textView); - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) - } - ); + var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); - Top.Add (statusBar); + appWindow.Add (statusBar); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } /// @@ -236,10 +243,10 @@ public class SyntaxHighlighting : Scenario foreach (Rune rune in csName.EnumerateRunes ()) { - runeCells.Add (new RuneCell { Rune = rune, ColorScheme = color.Value }); + runeCells.Add (new() { Rune = rune, ColorScheme = color.Value }); } - runeCells.Add (new RuneCell { Rune = (Rune)'\n', ColorScheme = color.Value }); + runeCells.Add (new() { Rune = (Rune)'\n', ColorScheme = color.Value }); } if (File.Exists (_path)) @@ -260,10 +267,10 @@ public class SyntaxHighlighting : Scenario { ClearAllEvents (); - _green = new ColorScheme (new Attribute (Color.Green, Color.Black)); - _blue = new ColorScheme (new Attribute (Color.Blue, Color.Black)); - _magenta = new ColorScheme (new Attribute (Color.Magenta, Color.Black)); - _white = new ColorScheme (new Attribute (Color.White, Color.Black)); + _green = new (new Attribute (Color.Green, Color.Black)); + _blue = new (new Attribute (Color.Blue, Color.Black)); + _magenta = new (new Attribute (Color.Magenta, Color.Black)); + _white = new (new Attribute (Color.White, Color.Black)); _textView.ColorScheme = _white; _textView.Text = @@ -342,7 +349,7 @@ public class SyntaxHighlighting : Scenario private string IdxToWord (List line, int idx) { string [] words = Regex.Split ( - new string (line.Select (r => (char)r.Value).ToArray ()), + new (line.Select (r => (char)r.Value).ToArray ()), "\\b" ); diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index 838619b29..f0309a092 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -15,76 +15,78 @@ public class TabViewExample : Scenario private MenuItem _miTabsOnBottom; private TabView _tabView; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); var menu = new MenuBar { Menus = [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ("_Add Blank Tab", "", AddBlankTab), - new ( - "_Clear SelectedTab", - "", - () => _tabView.SelectedTab = null - ), - new ("_Quit", "", Quit) - } - ), - new MenuBarItem ( - "_View", - new [] - { - _miShowTopLine = - new MenuItem ("_Show Top Line", "", ShowTopLine) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - _miShowBorder = - new MenuItem ("_Show Border", "", ShowBorder) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - _miTabsOnBottom = - new MenuItem ("_Tabs On Bottom", "", SetTabsOnBottom) - { - Checked = false, CheckType = MenuItemCheckStyle.Checked - }, - _miShowTabViewBorder = - new MenuItem ( - "_Show TabView Border", - "", - ShowTabViewBorder - ) { Checked = true, CheckType = MenuItemCheckStyle.Checked } - } - ) + new ( + "_File", + new MenuItem [] + { + new ("_Add Blank Tab", "", AddBlankTab), + new ( + "_Clear SelectedTab", + "", + () => _tabView.SelectedTab = null + ), + new ("_Quit", "", Quit) + } + ), + new ( + "_View", + new [] + { + _miShowTopLine = + new ("_Show Top Line", "", ShowTopLine) + { + Checked = true, CheckType = MenuItemCheckStyle.Checked + }, + _miShowBorder = + new ("_Show Border", "", ShowBorder) + { + Checked = true, CheckType = MenuItemCheckStyle.Checked + }, + _miTabsOnBottom = + new ("_Tabs On Bottom", "", SetTabsOnBottom) + { + Checked = false, CheckType = MenuItemCheckStyle.Checked + }, + _miShowTabViewBorder = + new ( + "_Show TabView Border", + "", + ShowTabViewBorder + ) { Checked = true, CheckType = MenuItemCheckStyle.Checked } + } + ) ] }; - Top.Add (menu); + appWindow.Add (menu); - _tabView = new TabView + _tabView = new() { X = 0, - Y = 0, + Y = 1, Width = 60, Height = 20, BorderStyle = LineStyle.Single }; - _tabView.AddTab (new Tab { DisplayText = "Tab1", View = new Label { Text = "hodor!" } }, false); - _tabView.AddTab (new Tab { DisplayText = "Tab2", View = new TextField { Text = "durdur" } }, false); - _tabView.AddTab (new Tab { DisplayText = "Interactive Tab", View = GetInteractiveTab () }, false); - _tabView.AddTab (new Tab { DisplayText = "Big Text", View = GetBigTextFileTab () }, false); + _tabView.AddTab (new() { DisplayText = "Tab1", View = new Label { Text = "hodor!" } }, false); + _tabView.AddTab (new() { DisplayText = "Tab2", View = new TextField { Text = "durdur" } }, false); + _tabView.AddTab (new() { DisplayText = "Interactive Tab", View = GetInteractiveTab () }, false); + _tabView.AddTab (new() { DisplayText = "Big Text", View = GetBigTextFileTab () }, false); _tabView.AddTab ( - new Tab + new() { DisplayText = "Long name Tab, I mean seriously long. Like you would not believe how long this tab's name is its just too much really woooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooowwww thats long", @@ -98,7 +100,7 @@ public class TabViewExample : Scenario ); _tabView.AddTab ( - new Tab + new() { DisplayText = "Les Mise" + '\u0301' + "rables", View = new Label { Text = "This tab name is unicode" } }, @@ -106,7 +108,7 @@ public class TabViewExample : Scenario ); _tabView.AddTab ( - new Tab + new() { DisplayText = "Les Mise" + '\u0328' + '\u0301' + "rables", View = new Label @@ -121,21 +123,21 @@ public class TabViewExample : Scenario for (var i = 0; i < 100; i++) { _tabView.AddTab ( - new Tab { DisplayText = $"Tab{i}", View = new Label { Text = $"Welcome to tab {i}" } }, + new() { DisplayText = $"Tab{i}", View = new Label { Text = $"Welcome to tab {i}" } }, false ); } _tabView.SelectedTab = _tabView.Tabs.First (); - Win.Add (_tabView); + appWindow.Add (_tabView); var frameRight = new FrameView { X = Pos.Right (_tabView), - Y = 0, + Y = 1, Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill (1), Title = "About" }; @@ -148,14 +150,14 @@ public class TabViewExample : Scenario } ); - Win.Add (frameRight); + appWindow.Add (frameRight); var frameBelow = new FrameView { X = 0, Y = Pos.Bottom (_tabView), Width = _tabView.Width, - Height = Dim.Fill (), + Height = Dim.Fill (1), Title = "Bottom Frame" }; @@ -169,22 +171,21 @@ public class TabViewExample : Scenario } ); - Win.Add (frameBelow); + appWindow.Add (frameBelow); - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - Quit - ) - } - ); - Top.Add (statusBar); + var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); + appWindow.Add (statusBar); + + // Run - Start the application. + Application.Run (appWindow); + + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } - private void AddBlankTab () { _tabView.AddTab (new Tab (), false); } + private void AddBlankTab () { _tabView.AddTab (new (), false); } private View GetBigTextFileTab () { diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 70c3151b8..df86aceaf 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -427,13 +427,15 @@ public class TableEditor : Scenario return dt; } - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); - _tableView = new() { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (1) }; + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); + + _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; var menu = new MenuBar { @@ -669,48 +671,48 @@ public class TableEditor : Scenario ] }; - Top.Add (menu); - - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - KeyCode.F2, - "~F2~ OpenExample", - () => OpenExample (true) - ), - new ( - KeyCode.F3, - "~F3~ CloseExample", - () => CloseExample () - ), - new ( - KeyCode.F4, - "~F4~ OpenSimple", - () => OpenSimple (true) - ), - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) - } - ); - Top.Add (statusBar); - - Win.Add (_tableView); + appWindow.Add (menu); var selectedCellLabel = new Label { - X = 0, - Y = Pos.Bottom (_tableView), - Text = "0,0", - - Width = Dim.Fill (), - TextAlignment = Alignment.End + Text = "0,0" }; - Win.Add (selectedCellLabel); + var statusBar = new StatusBar ( + [ + new ( + Application.QuitKey, + "Quit", + Quit + ), + new ( + Key.F2, + "OpenExample", + () => OpenExample (true) + ), + new ( + Key.F3, + "CloseExample", + CloseExample + ), + new ( + Key.F4, + "OpenSimple", + () => OpenSimple (true) + ), + new () + { + HelpText = "Cell:", + CommandView = selectedCellLabel + } + ] + ) + { + AlignmentModes = AlignmentModes.IgnoreFirstOrLast + }; + appWindow.Add (statusBar); + + appWindow.Add (_tableView); _tableView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_tableView.SelectedRow},{_tableView.SelectedColumn}"; }; _tableView.CellActivated += EditCurrentCell; @@ -718,27 +720,27 @@ public class TableEditor : Scenario SetupScrollBar (); - _redColorScheme = new() + _redColorScheme = new () { - Disabled = Win.ColorScheme.Disabled, - HotFocus = Win.ColorScheme.HotFocus, - Focus = Win.ColorScheme.Focus, - Normal = new (Color.Red, Win.ColorScheme.Normal.Background) + Disabled = appWindow.ColorScheme.Disabled, + HotFocus = appWindow.ColorScheme.HotFocus, + Focus = appWindow.ColorScheme.Focus, + Normal = new (Color.Red, appWindow.ColorScheme.Normal.Background) }; - _alternatingColorScheme = new() + _alternatingColorScheme = new () { - Disabled = Win.ColorScheme.Disabled, - HotFocus = Win.ColorScheme.HotFocus, - Focus = Win.ColorScheme.Focus, + Disabled = appWindow.ColorScheme.Disabled, + HotFocus = appWindow.ColorScheme.HotFocus, + Focus = appWindow.ColorScheme.Focus, Normal = new (Color.White, Color.BrightBlue) }; - _redColorSchemeAlt = new() + _redColorSchemeAlt = new () { - Disabled = Win.ColorScheme.Disabled, - HotFocus = Win.ColorScheme.HotFocus, - Focus = Win.ColorScheme.Focus, + Disabled = appWindow.ColorScheme.Disabled, + HotFocus = appWindow.ColorScheme.HotFocus, + Focus = appWindow.ColorScheme.Focus, Normal = new (Color.Red, Color.BrightBlue) }; @@ -768,6 +770,13 @@ public class TableEditor : Scenario }; _tableView.KeyBindings.Add (Key.Space, Command.Accept); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } protected override void Dispose (bool disposing) @@ -1017,7 +1026,7 @@ public class TableEditor : Scenario _tableView, "Name", tree, - new() + new () { { "Extension", f => f.Extension }, { "CreationTime", f => f.CreationTime }, diff --git a/UICatalog/Scenarios/TextAlignmentAndDirection.cs b/UICatalog/Scenarios/TextAlignmentAndDirection.cs index eb4f08bf1..06f52f063 100644 --- a/UICatalog/Scenarios/TextAlignmentAndDirection.cs +++ b/UICatalog/Scenarios/TextAlignmentAndDirection.cs @@ -592,6 +592,7 @@ public class TextAlignmentAndDirection : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); void ToggleJustify (bool oldValue, bool wasJustOptions = false) { diff --git a/UICatalog/Scenarios/TextFormatterDemo.cs b/UICatalog/Scenarios/TextFormatterDemo.cs index 81bf6a839..a3f36bf1d 100644 --- a/UICatalog/Scenarios/TextFormatterDemo.cs +++ b/UICatalog/Scenarios/TextFormatterDemo.cs @@ -144,5 +144,6 @@ public class TextFormatterDemo : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/TextViewAutocompletePopup.cs b/UICatalog/Scenarios/TextViewAutocompletePopup.cs index 8dbbdddf8..16de9c1f6 100644 --- a/UICatalog/Scenarios/TextViewAutocompletePopup.cs +++ b/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -13,17 +13,22 @@ public class TextViewAutocompletePopup : Scenario private int _height = 10; private MenuItem _miMultiline; private MenuItem _miWrap; - private StatusItem _siMultiline; - private StatusItem _siWrap; + private Shortcut _siMultiline; + private Shortcut _siWrap; private TextView _textViewBottomLeft; private TextView _textViewBottomRight; private TextView _textViewCentered; private TextView _textViewTopLeft; private TextView _textViewTopRight; - public override void Setup () + public override void Main () { - Win.Title = GetName (); + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); + var width = 20; var text = " jamp jemp jimp jomp jump"; @@ -31,44 +36,52 @@ public class TextViewAutocompletePopup : Scenario { Menus = [ - new MenuBarItem ( - "_File", - new [] - { - _miMultiline = - new MenuItem ( - "_Multiline", - "", - () => Multiline () - ) { CheckType = MenuItemCheckStyle.Checked }, - _miWrap = new MenuItem ( - "_Word Wrap", - "", - () => WordWrap () - ) { CheckType = MenuItemCheckStyle.Checked }, - new ("_Quit", "", () => Quit ()) - } - ) + new ( + "_File", + new [] + { + _miMultiline = + new ( + "_Multiline", + "", + () => Multiline () + ) { CheckType = MenuItemCheckStyle.Checked }, + _miWrap = new ( + "_Word Wrap", + "", + () => WordWrap () + ) { CheckType = MenuItemCheckStyle.Checked }, + new ("_Quit", "", () => Quit ()) + } + ) ] }; - Top.Add (menu); + appWindow.Add (menu); - _textViewTopLeft = new TextView { Width = width, Height = _height, Text = text }; + _textViewTopLeft = new() + { + Y = 1, + Width = width, Height = _height, Text = text + }; _textViewTopLeft.DrawContent += TextViewTopLeft_DrawContent; - Win.Add (_textViewTopLeft); + appWindow.Add (_textViewTopLeft); - _textViewTopRight = new TextView { X = Pos.AnchorEnd (width), Width = width, Height = _height, Text = text }; + _textViewTopRight = new() + { + X = Pos.AnchorEnd (width), Y = 1, + Width = width, Height = _height, Text = text + }; _textViewTopRight.DrawContent += TextViewTopRight_DrawContent; - Win.Add (_textViewTopRight); + appWindow.Add (_textViewTopRight); - _textViewBottomLeft = new TextView + _textViewBottomLeft = new() { Y = Pos.AnchorEnd (_height), Width = width, Height = _height, Text = text }; _textViewBottomLeft.DrawContent += TextViewBottomLeft_DrawContent; - Win.Add (_textViewBottomLeft); + appWindow.Add (_textViewBottomLeft); - _textViewBottomRight = new TextView + _textViewBottomRight = new() { X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (_height), @@ -77,9 +90,9 @@ public class TextViewAutocompletePopup : Scenario Text = text }; _textViewBottomRight.DrawContent += TextViewBottomRight_DrawContent; - Win.Add (_textViewBottomRight); + appWindow.Add (_textViewBottomRight); - _textViewCentered = new TextView + _textViewCentered = new() { X = Pos.Center (), Y = Pos.Center (), @@ -88,7 +101,7 @@ public class TextViewAutocompletePopup : Scenario Text = text }; _textViewCentered.DrawContent += TextViewCentered_DrawContent; - Win.Add (_textViewCentered); + appWindow.Add (_textViewCentered); _miMultiline.Checked = _textViewTopLeft.Multiline; _miWrap.Checked = _textViewTopLeft.WordWrap; @@ -98,16 +111,24 @@ public class TextViewAutocompletePopup : Scenario { new ( Application.QuitKey, - $"{Application.QuitKey} to Quit", + "Quit", () => Quit () ), - _siMultiline = new StatusItem (KeyCode.Null, "", null), - _siWrap = new StatusItem (KeyCode.Null, "", null) + _siMultiline = new (Key.Empty, "", null), + _siWrap = new (Key.Empty, "", null) } ); - Top.Add (statusBar); + appWindow.Add (statusBar); - Win.LayoutStarted += Win_LayoutStarted; + appWindow.LayoutStarted += Win_LayoutStarted; + + // Run - Start the application. + Application.Run (appWindow); + + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } private void Multiline () @@ -133,6 +154,7 @@ public class TextViewAutocompletePopup : Scenario } private void SetMultilineStatusText () { _siMultiline.Title = $"Multiline: {_miMultiline.Checked}"; } + private void SetWrapStatusText () { _siWrap.Title = $"WordWrap: {_miWrap.Checked}"; } private void TextViewBottomLeft_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewBottomLeft); } private void TextViewBottomRight_DrawContent (object sender, DrawEventArgs e) { SetAllSuggestions (_textViewBottomRight); } diff --git a/UICatalog/Scenarios/TreeUseCases.cs b/UICatalog/Scenarios/TreeUseCases.cs index f306d1a91..57cee729a 100644 --- a/UICatalog/Scenarios/TreeUseCases.cs +++ b/UICatalog/Scenarios/TreeUseCases.cs @@ -11,11 +11,13 @@ public class TreeUseCases : Scenario { private View _currentTree; - public override void Setup () + public override void Main () { - Win.Title = GetName (); - Win.Y = 1; // menu - Win.Height = Dim.Fill (1); // status bar + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Toplevel appWindow = new (); var menu = new MenuBar { @@ -47,23 +49,23 @@ public class TreeUseCases : Scenario ] }; - Top.Add (menu); + appWindow.Add (menu); - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - Application.QuitKey, - $"{Application.QuitKey} to Quit", - () => Quit () - ) - } - ); + var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); - Top.Add (statusBar); + appWindow.Add (statusBar); + + appWindow.Ready += (sender, args) => + // Start with the most basic use case + LoadSimpleNodes (); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); - // Start with the most basic use case - LoadSimpleNodes (); } private void LoadArmies (bool useDelegate) @@ -76,11 +78,11 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Win.Remove (_currentTree); + Application.Top.Remove (_currentTree); _currentTree.Dispose (); } - TreeView tree = new () { X = 0, Y = 0, Width = 40, Height = 20 }; + TreeView tree = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; if (useDelegate) { @@ -96,7 +98,7 @@ public class TreeUseCases : Scenario tree.TreeBuilder = new GameObjectTreeBuilder (); } - Win.Add (tree); + Application.Top.Add (tree); tree.AddObject (army1); @@ -116,13 +118,13 @@ public class TreeUseCases : Scenario if (_currentTree != null) { - Win.Remove (_currentTree); + Application.Top.Remove (_currentTree); _currentTree.Dispose (); } - var tree = new TreeView { X = 0, Y = 0, Width = 40, Height = 20 }; + var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill(), Height = Dim.Fill (1) }; - Win.Add (tree); + Application.Top.Add (tree); tree.AddObject (myHouse); @@ -133,13 +135,13 @@ public class TreeUseCases : Scenario { if (_currentTree != null) { - Win.Remove (_currentTree); + Application.Top.Remove (_currentTree); _currentTree.Dispose (); } - var tree = new TreeView { X = 0, Y = 0, Width = 40, Height = 20 }; + var tree = new TreeView { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - Win.Add (tree); + Application.Top.Add (tree); var root1 = new TreeNode ("Root1"); root1.Children.Add (new TreeNode ("Child1.1")); diff --git a/UICatalog/Scenarios/TrueColors.cs b/UICatalog/Scenarios/TrueColors.cs index 266213029..992ace392 100644 --- a/UICatalog/Scenarios/TrueColors.cs +++ b/UICatalog/Scenarios/TrueColors.cs @@ -125,6 +125,8 @@ public class TrueColors : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); + return; void SetupGradient (string name, int x, ref int y, Func colorFunc) diff --git a/UICatalog/Scenarios/Unicode.cs b/UICatalog/Scenarios/Unicode.cs index 61dc9166b..384369e12 100644 --- a/UICatalog/Scenarios/Unicode.cs +++ b/UICatalog/Scenarios/Unicode.cs @@ -10,7 +10,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] public class UnicodeInMenu : Scenario { - public override void Setup () + public override void Main () { var unicode = "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ\nτὸ σπίτι φτωχικὸ στὶς ἀμμουδιὲς τοῦ Ὁμήρου.\nΜονάχη ἔγνοια ἡ γλῶσσα μου στὶς ἀμμουδιὲς τοῦ Ὁμήρου."; @@ -28,6 +28,15 @@ public class UnicodeInMenu : Scenario CM.Glyphs.HorizontalEllipsis }"; + // Init + Application.Init (); + + // Setup - Create a top-level application window and configure it. + Window appWindow = new () + { + Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" + }; + var menu = new MenuBar { Menus = @@ -60,24 +69,24 @@ public class UnicodeInMenu : Scenario ) ] }; - Top.Add (menu); + appWindow.Add (menu); var statusBar = new StatusBar ( - new StatusItem [] + new Shortcut [] { new ( Application.QuitKey, - $"{Application.QuitKey} Выход", + "Выход", () => Application.RequestStop () ), - new (KeyCode.Null, "~F2~ Создать", null), - new (KeyCode.Null, "~F3~ Со_хранить", null) + new (Key.F2, "Создать", null), + new (Key.F3, "Со_хранить", null) } ); - Top.Add (statusBar); + appWindow.Add (statusBar); var label = new Label { X = 0, Y = 1, Text = "Label:" }; - Win.Add (label); + appWindow.Add (label); var testlabel = new Label { @@ -87,16 +96,16 @@ public class UnicodeInMenu : Scenario Width = Dim.Percent (50), Text = gitString }; - Win.Add (testlabel); + appWindow.Add (testlabel); - label = new() { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Label (CanFocus):" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Label (CanFocus):" }; + appWindow.Add (label); var sb = new StringBuilder (); sb.Append ('e'); sb.Append ('\u0301'); sb.Append ('\u0301'); - testlabel = new() + testlabel = new () { X = 20, Y = Pos.Y (label), @@ -106,14 +115,14 @@ public class UnicodeInMenu : Scenario HotKeySpecifier = new ('&'), Text = $"Should be [e with two accents, but isn't due to #2616]: [{sb}]" }; - Win.Add (testlabel); - label = new() { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Button:" }; - Win.Add (label); + appWindow.Add (testlabel); + label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "Button:" }; + appWindow.Add (label); var button = new Button { X = 20, Y = Pos.Y (label), Text = "A123456789♥♦♣♠JQK" }; - Win.Add (button); + appWindow.Add (button); - label = new() { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "CheckBox:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 1, Text = "CheckBox:" }; + appWindow.Add (label); var checkBox = new CheckBox { @@ -135,27 +144,27 @@ public class UnicodeInMenu : Scenario TextAlignment = Alignment.End, Text = $"End - {gitString}" }; - Win.Add (checkBox, checkBoxRight); + appWindow.Add (checkBox, checkBoxRight); - label = new() { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (checkBoxRight) + 1, Text = "ComboBox:" }; + appWindow.Add (label); var comboBox = new ComboBox { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) }; comboBox.SetSource (new ObservableCollection { gitString, "Со_хранить" }); - Win.Add (comboBox); + appWindow.Add (comboBox); comboBox.Text = gitString; - label = new() { X = Pos.X (label), Y = Pos.Bottom (label) + 2, Text = "HexView:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (label) + 2, Text = "HexView:" }; + appWindow.Add (label); var hexView = new HexView (new MemoryStream (Encoding.ASCII.GetBytes (gitString + " Со_хранить"))) { X = 20, Y = Pos.Y (label), Width = Dim.Percent (60), Height = 5 }; - Win.Add (hexView); + appWindow.Add (hexView); - label = new() { X = Pos.X (label), Y = Pos.Bottom (hexView) + 1, Text = "ListView:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (hexView) + 1, Text = "ListView:" }; + appWindow.Add (label); var listView = new ListView { @@ -167,10 +176,10 @@ public class UnicodeInMenu : Scenario ["item #1", gitString, "Со_хранить", unicode] ) }; - Win.Add (listView); + appWindow.Add (listView); - label = new() { X = Pos.X (label), Y = Pos.Bottom (listView) + 1, Text = "RadioGroup:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (listView) + 1, Text = "RadioGroup:" }; + appWindow.Add (label); var radioGroup = new RadioGroup { @@ -179,19 +188,19 @@ public class UnicodeInMenu : Scenario Width = Dim.Percent (60), RadioLabels = new [] { "item #1", gitString, "Со_хранить", "𝔽𝕆𝕆𝔹𝔸ℝ" } }; - Win.Add (radioGroup); + appWindow.Add (radioGroup); - label = new() { X = Pos.X (label), Y = Pos.Bottom (radioGroup) + 1, Text = "TextField:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (radioGroup) + 1, Text = "TextField:" }; + appWindow.Add (label); var textField = new TextField { X = 20, Y = Pos.Y (label), Width = Dim.Percent (60), Text = gitString + " = Со_хранить" }; - Win.Add (textField); + appWindow.Add (textField); - label = new() { X = Pos.X (label), Y = Pos.Bottom (textField) + 1, Text = "TextView:" }; - Win.Add (label); + label = new () { X = Pos.X (label), Y = Pos.Bottom (textField) + 1, Text = "TextView:" }; + appWindow.Add (label); var textView = new TextView { @@ -201,6 +210,14 @@ public class UnicodeInMenu : Scenario Height = 5, Text = unicode }; - Win.Add (textView); + appWindow.Add (textView); + + // Run - Start the application. + Application.Run (appWindow); + + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index b28fbb8ba..a35aa3bed 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -248,5 +248,6 @@ public class ViewExperiments : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/VkeyPacketSimulator.cs b/UICatalog/Scenarios/VkeyPacketSimulator.cs index 43aee6bf3..983fe2d29 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -104,7 +104,7 @@ public class VkeyPacketSimulator : Scenario if (_outputStarted) { // If the key wasn't handled by the TextView will popup a Dialog with the keys pressed. - bool? handled = tvOutput.OnInvokingKeyBindings (e); + bool? handled = tvOutput.OnInvokingKeyBindings (e, KeyBindingScope.HotKey | KeyBindingScope.Focused); if (handled == null || handled == false) { @@ -260,6 +260,10 @@ public class VkeyPacketSimulator : Scenario void Win_LayoutComplete (object sender, LayoutEventArgs obj) { + if (inputHorizontalRuler.Viewport.Width == 0 || inputVerticalRuler.Viewport.Height == 0) + { + return; + } inputHorizontalRuler.Text = outputHorizontalRuler.Text = ruler.Repeat ( (int)Math.Ceiling ( diff --git a/UICatalog/Scenarios/WindowsAndFrameViews.cs b/UICatalog/Scenarios/WindowsAndFrameViews.cs index f522420c6..b8714d079 100644 --- a/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -207,5 +207,6 @@ public class WindowsAndFrameViews : Scenario Application.Run (app); app.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/Scenarios/WizardAsView.cs b/UICatalog/Scenarios/WizardAsView.cs index 817cccd3b..b4d704f34 100644 --- a/UICatalog/Scenarios/WizardAsView.cs +++ b/UICatalog/Scenarios/WizardAsView.cs @@ -6,7 +6,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Wizards")] public class WizardAsView : Scenario { - public override void Init () + public override void Main () { Application.Init (); @@ -52,8 +52,9 @@ public class WizardAsView : Scenario ) ] }; - Top = new (); - Top.Add (menu); + + Toplevel topLevel = new (); + topLevel.Add (menu); // No need for a Title because the border is disabled var wizard = new Wizard { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; @@ -139,13 +140,9 @@ public class WizardAsView : Scenario lastStep.HelpText = "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing Esc will cancel."; - Top.Add (wizard); - Application.Run (Top); - } - - public override void Run () - { - // Do nothing in the override because we call Application.Run above - // (just to make it clear how the Top is being run and not the Wizard). + topLevel.Add (wizard); + Application.Run (topLevel); + topLevel.Dispose (); + Application.Shutdown (); } } diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 6c57e4d67..12ccb9a3b 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -371,12 +371,13 @@ internal class UICatalogApp public class UICatalogTopLevel : Toplevel { public ListView CategoryList; - public StatusItem DriverName; - public MenuItem? miForce16Colors; - public MenuItem? miIsMenuBorderDisabled; - public MenuItem? miIsMouseDisabled; - public MenuItem? miUseSubMenusSingleFrame; - public StatusItem OS; + public MenuItem? MiForce16Colors; + public MenuItem? MiIsMenuBorderDisabled; + public MenuItem? MiIsMouseDisabled; + public MenuItem? MiUseSubMenusSingleFrame; + public Shortcut? ShForce16Colors; + //public Shortcut? ShDiagnostics; + public Shortcut? ShVersion; // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView @@ -446,42 +447,74 @@ internal class UICatalogApp ] }; - DriverName = new (Key.Empty, "Driver:", null); - OS = new (Key.Empty, "OS:", null); - - StatusBar = new () { Visible = ShowStatusBar }; - - StatusBar.Items = new [] + StatusBar = new () { - new ( - Application.QuitKey, - $"~{Application.QuitKey} to quit", - () => - { - if (_selectedScenario is null) - { - // This causes GetScenarioToRun to return null - _selectedScenario = null; - RequestStop (); - } - } - ), - new ( - Key.F10, - "~F10~ Status Bar", - () => - { - StatusBar.Visible = !StatusBar.Visible; - - //ContentPane!.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); - LayoutSubviews (); - SetSubViewNeedsDisplay (); - } - ), - DriverName, - OS + Visible = ShowStatusBar, }; + if (StatusBar is { }) + { + ShVersion = new () + { + Title = "Version Info", + CanFocus = false, + + }; + + Shortcut statusBarShortcut = new Shortcut () + { + Key = Key.F10, + Title = "Show/Hide Status Bar", + }; + statusBarShortcut.Accept += (sender, args) => { StatusBar.Visible = !StatusBar.Visible; }; + + ShForce16Colors = new Shortcut () + { + CommandView = new CheckBox () + { + Title = "16 color mode", + Checked = Application.Force16Colors, + CanFocus = false, + }, + HelpText = "", + Key = Key.F6, + }; + + ShForce16Colors.Accept += (sender, args) => + { + ((CheckBox)ShForce16Colors.CommandView).Checked = + Application.Force16Colors = (bool)!((CheckBox)ShForce16Colors.CommandView).Checked!; + MiForce16Colors.Checked = Application.Force16Colors; + Application.Refresh (); + + }; + + //ShDiagnostics = new Shortcut () + //{ + // HelpText = "Diagnostic flags", + // CommandView = new RadioGroup() + // { + // RadioLabels = ["Off", "Ruler", "Padding", "MouseEnter"], + + // CanFocus = false, + // Orientation = Orientation.Vertical, + // } + //}; + + StatusBar.Add ( + new Shortcut () + { + Title = "Quit", + Key = Application.QuitKey, + }, + statusBarShortcut, + ShForce16Colors, + + //ShDiagnostics, + ShVersion + ); + } + // Create the Category list view. This list never changes. CategoryList = new () { @@ -507,7 +540,7 @@ internal class UICatalogApp X = Pos.Right (CategoryList) - 1, Y = 1, Width = Dim.Fill (), - Height = Dim.Fill (1), + Height = Dim.Height (CategoryList), //AllowsMarking = false, CanFocus = true, @@ -585,7 +618,11 @@ internal class UICatalogApp Add (ScenarioList); Add (MenuBar); - Add (StatusBar); + + if (StatusBar is { }) + { + Add (StatusBar); + } Loaded += LoadedHandler; Unloaded += UnloadedHandler; @@ -620,16 +657,14 @@ internal class UICatalogApp ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; MenuBar.Menus [0].Children [0].Shortcut = (KeyCode)Application.QuitKey; - StatusBar.Items [0].Shortcut = Application.QuitKey; - StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit"; - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; + if (StatusBar is { }) + { + ((Shortcut)StatusBar.Subviews [0]).Key = Application.QuitKey; + StatusBar.Visible = ShowStatusBar; + } - int height = ShowStatusBar ? 1 : 0; // + (MenuBar.Visible ? 1 : 0); - - //ContentPane.Height = Dim.Fill (height); - - StatusBar.Visible = ShowStatusBar; + MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; Application.Top.SetNeedsDisplay (); } @@ -894,22 +929,22 @@ internal class UICatalogApp private MenuItem [] CreateDisabledEnabledMenuBorder () { List menuItems = new (); - miIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" }; + MiIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" }; - miIsMenuBorderDisabled.Shortcut = - (KeyCode)new Key (miIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt + MiIsMenuBorderDisabled.Shortcut = + (KeyCode)new Key (MiIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt .WithCtrl.NoShift; - miIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; + MiIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; - miIsMenuBorderDisabled.Action += () => + MiIsMenuBorderDisabled.Action += () => { - miIsMenuBorderDisabled.Checked = (bool)!miIsMenuBorderDisabled.Checked!; + MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!; - MenuBar.MenusBorderStyle = !(bool)miIsMenuBorderDisabled.Checked + MenuBar.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked ? LineStyle.Single : LineStyle.None; }; - menuItems.Add (miIsMenuBorderDisabled); + menuItems.Add (MiIsMenuBorderDisabled); return menuItems.ToArray (); } @@ -917,18 +952,18 @@ internal class UICatalogApp private MenuItem [] CreateDisabledEnabledMouseItems () { List menuItems = new (); - miIsMouseDisabled = new () { Title = "_Disable Mouse" }; + MiIsMouseDisabled = new () { Title = "_Disable Mouse" }; - miIsMouseDisabled.Shortcut = - (KeyCode)new Key (miIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift; - miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; + MiIsMouseDisabled.Shortcut = + (KeyCode)new Key (MiIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift; + MiIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; - miIsMouseDisabled.Action += () => + MiIsMouseDisabled.Action += () => { - miIsMouseDisabled.Checked = - Application.IsMouseDisabled = (bool)!miIsMouseDisabled.Checked!; + MiIsMouseDisabled.Checked = + Application.IsMouseDisabled = (bool)!MiIsMouseDisabled.Checked!; }; - menuItems.Add (miIsMouseDisabled); + menuItems.Add (MiIsMouseDisabled); return menuItems.ToArray (); } @@ -937,20 +972,20 @@ internal class UICatalogApp private MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame () { List menuItems = new (); - miUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" }; + MiUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" }; - miUseSubMenusSingleFrame.Shortcut = KeyCode.CtrlMask + MiUseSubMenusSingleFrame.Shortcut = KeyCode.CtrlMask | KeyCode.AltMask - | (KeyCode)miUseSubMenusSingleFrame!.Title!.Substring (8, 1) [ + | (KeyCode)MiUseSubMenusSingleFrame!.Title!.Substring (8, 1) [ 0]; - miUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; + MiUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; - miUseSubMenusSingleFrame.Action += () => + MiUseSubMenusSingleFrame.Action += () => { - miUseSubMenusSingleFrame.Checked = (bool)!miUseSubMenusSingleFrame.Checked!; - MenuBar.UseSubMenusSingleFrame = (bool)miUseSubMenusSingleFrame.Checked; + MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!; + MenuBar.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked; }; - menuItems.Add (miUseSubMenusSingleFrame); + menuItems.Add (MiUseSubMenusSingleFrame); return menuItems.ToArray (); } @@ -959,21 +994,22 @@ internal class UICatalogApp { List menuItems = new (); - miForce16Colors = new () + MiForce16Colors = new () { Title = "Force _16 Colors", Shortcut = (KeyCode)Key.F6, Checked = Application.Force16Colors, CanExecute = () => Application.Driver.SupportsTrueColor }; - miForce16Colors.CheckType |= MenuItemCheckStyle.Checked; + MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked; - miForce16Colors.Action += () => + MiForce16Colors.Action += () => { - miForce16Colors.Checked = Application.Force16Colors = (bool)!miForce16Colors.Checked!; + MiForce16Colors.Checked = Application.Force16Colors = (bool)!MiForce16Colors.Checked!; + ((CheckBox)ShForce16Colors!.CommandView!).Checked = Application.Force16Colors; Application.Refresh (); }; - menuItems.Add (miForce16Colors); + menuItems.Add (MiForce16Colors); return menuItems.ToArray (); } @@ -1000,11 +1036,12 @@ internal class UICatalogApp { ConfigChanged (); - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; - DriverName.Title = $"Driver: {Driver.GetVersionInfo ()}"; + MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - OS.Title = - $"OS: {RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}"; + if (ShVersion is { }) + { + ShVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver.GetVersionInfo ()}"; + } if (_selectedScenario != null) { @@ -1017,18 +1054,21 @@ internal class UICatalogApp ScenarioList.SetFocus (); } - StatusBar.VisibleChanged += (s, e) => - { - ShowStatusBar = StatusBar.Visible; + if (StatusBar is { }) + { + StatusBar.VisibleChanged += (s, e) => + { + ShowStatusBar = StatusBar.Visible; - int height = StatusBar.Visible ? 1 : 0; - CategoryList.Height = Dim.Fill (height); - ScenarioList.Height = Dim.Fill (height); + int height = StatusBar.Visible ? 1 : 0; + CategoryList.Height = Dim.Fill (height); + ScenarioList.Height = Dim.Fill (height); - // ContentPane.Height = Dim.Fill (height); - LayoutSubviews (); - SetSubViewNeedsDisplay (); - }; + // ContentPane.Height = Dim.Fill (height); + LayoutSubviews (); + SetSubViewNeedsDisplay (); + }; + } Loaded -= LoadedHandler; CategoryList.EnsureSelectedItemVisible (); diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 498842437..964fada3b 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using Microsoft.VisualBasic; +using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console @@ -147,8 +148,12 @@ public class ApplicationTests Shutdown (); } - [Fact] - public void Init_ResetState_Resets_Properties () + [Theory] + [InlineData (typeof (FakeDriver))] + [InlineData (typeof (NetDriver))] + [InlineData (typeof (WindowsDriver))] + [InlineData (typeof (CursesDriver))] + public void Init_ResetState_Resets_Properties (Type driverType) { ConfigurationManager.ThrowOnJsonErrors = true; @@ -156,7 +161,7 @@ public class ApplicationTests // Set some values - Application.Init (); + Application.Init (driverName: driverType.Name); Application._initialized = true; // Reset @@ -228,7 +233,7 @@ public class ApplicationTests Application.AlternateBackwardKey = Key.A; Application.AlternateForwardKey = Key.B; Application.QuitKey = Key.C; - Application.AddKeyBinding(Key.A, new View ()); + Application.AddKeyBinding (Key.A, new View ()); //Application.OverlappedChildren = new List (); //Application.OverlappedTop = @@ -266,6 +271,71 @@ public class ApplicationTests #endif } + [Theory] + [InlineData (typeof (FakeDriver))] + [InlineData (typeof (NetDriver))] + [InlineData (typeof (WindowsDriver))] + [InlineData (typeof (CursesDriver))] + public void Init_Shutdown_Fire_InitializedChanged (Type driverType) + { + bool initialized = false; + bool shutdown = false; + + Application.InitializedChanged += OnApplicationOnInitializedChanged; + + Application.Init (driverName: driverType.Name); + Assert.True (initialized); + Assert.False (shutdown); + + Application.Shutdown (); + Assert.True (initialized); + Assert.True (shutdown); + + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + return; + + void OnApplicationOnInitializedChanged (object s, StateEventArgs a) + { + if (a.NewValue) + { + initialized = true; + } + else + { + shutdown = true; + } + } + } + + + [Fact] + public void Run_Iteration_Fires () + { + int iteration = 0; + + Application.Init (new FakeDriver ()); + + Application.Iteration += Application_Iteration; + Application.Run ().Dispose (); + + Assert.Equal (1, iteration); + Application.Shutdown (); + + return; + + void Application_Iteration (object sender, IterationEventArgs e) + { + if (iteration > 0) + { + Assert.Fail (); + } + iteration++; + Application.RequestStop (); + } + } + + [Fact] public void Init_Unbalanced_Throws () { @@ -826,10 +896,10 @@ public class ApplicationTests Application.OnMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); Assert.Equal (w.Border, Application.MouseGrabView); - Assert.Equal (new Point (0,0), w.Frame.Location); + Assert.Equal (new Point (0, 0), w.Frame.Location); // Move down and to the right. - Application.OnMouseEvent (new () { Position = new (1,1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Application.OnMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); Assert.Equal (new Point (1, 1), w.Frame.Location); Application.End (rs); @@ -863,7 +933,7 @@ public class ApplicationTests #if DEBUG_IDISPOSABLE Assert.False (w.WasDisposed); - Exception exception = Record.Exception (() => Application.Shutdown ()); // Invalid - w has not been disposed. + Exception exception = Record.Exception (Application.Shutdown); // Invalid - w has not been disposed. Assert.NotNull (exception); w.Dispose (); @@ -902,7 +972,7 @@ public class ApplicationTests #if DEBUG_IDISPOSABLE Assert.Equal (top, Application.Top); Assert.False (top.WasDisposed); - Exception exception = Record.Exception (() => Application.Shutdown ()); + Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); Assert.False (top.WasDisposed); #endif @@ -934,7 +1004,7 @@ public class ApplicationTests Application.Run (null, driver); #if DEBUG_IDISPOSABLE Assert.False (Application.Top.WasDisposed); - Exception exception = Record.Exception (() => Application.Shutdown ()); + Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); Assert.False (Application.Top.WasDisposed); @@ -949,21 +1019,31 @@ public class ApplicationTests } [Fact] - public void Run_t_Creates_Top_Without_Init () + public void Run_t_Does_Not_Creates_Top_Without_Init () { + // When a Toplevel is created it must already have all the Application configuration loaded + // This is only possible by two ways: + // 1 - Using Application.Init first + // 2 - Using Application.Run() or Application.Run() + // The Application.Run(new(Toplevel)) must always call Application.Init() first because + // the new(Toplevel) may be a derived class that is possible using Application static + // properties that is only available after the Application.Init was called var driver = new FakeDriver (); Assert.Null (Application.Top); + Assert.Throws (() => Application.Run (new Toplevel ())); + + Application.Init (driver); Application.Iteration += (s, e) => { Assert.NotNull (Application.Top); Application.RequestStop (); }; - Application.Run (new (), null, driver); + Application.Run (new Toplevel ()); #if DEBUG_IDISPOSABLE Assert.False (Application.Top.WasDisposed); - Exception exception = Record.Exception (() => Application.Shutdown ()); + Exception exception = Record.Exception (Application.Shutdown); Assert.NotNull (exception); Assert.False (Application.Top.WasDisposed); @@ -1014,4 +1094,123 @@ public class ApplicationTests } #endregion + + + private object _timeoutLock; + + [Fact] + public void AddTimeout_Fires () + { + Assert.Null (_timeoutLock); + _timeoutLock = new object (); + + uint timeoutTime = 250; + bool initialized = false; + int iteration = 0; + bool shutdown = false; + object timeout = null; + int timeoutCount = 0; + + Application.InitializedChanged += OnApplicationOnInitializedChanged; + + Application.Init (new FakeDriver ()); + Assert.True (initialized); + Assert.False (shutdown); + + _output.WriteLine ("Application.Run ().Dispose ().."); + Application.Run ().Dispose (); + _output.WriteLine ("Back from Application.Run ().Dispose ()"); + + Assert.True (initialized); + Assert.False (shutdown); + + Assert.Equal (1, timeoutCount); + Application.Shutdown (); + + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (timeout is { }) + { + Application.RemoveTimeout (timeout); + timeout = null; + } + } + + Assert.True (initialized); + Assert.True (shutdown); + +#if DEBUG_IDISPOSABLE + Assert.Empty (Responder.Instances); +#endif + lock (_timeoutLock) + { + _timeoutLock = null; + } + + return; + + void OnApplicationOnInitializedChanged (object s, StateEventArgs a) + { + if (a.NewValue) + { + Application.Iteration += OnApplicationOnIteration; + initialized = true; + + lock (_timeoutLock) + { + _output.WriteLine ($"Setting timeout for {timeoutTime}ms"); + timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (timeoutTime), TimeoutCallback); + } + + } + else + { + Application.Iteration -= OnApplicationOnIteration; + shutdown = true; + } + } + + bool TimeoutCallback () + { + lock (_timeoutLock) + { + _output.WriteLine ($"TimeoutCallback. Count: {++timeoutCount}. Application Iteration: {iteration}"); + if (timeout is { }) + { + _output.WriteLine ($" Nulling timeout."); + timeout = null; + } + } + + // False means "don't re-do timer and remove it" + return false; + } + + void OnApplicationOnIteration (object s, IterationEventArgs a) + { + lock (_timeoutLock) + { + if (timeoutCount > 0) + { + _output.WriteLine ($"Iteration #{iteration} - Timeout fired. Calling Application.RequestStop."); + Application.RequestStop (); + + return; + } + } + iteration++; + + // Simulate a delay + Thread.Sleep ((int)timeoutTime / 10); + + // Worst case scenario - something went wrong + if (Application._initialized && iteration > 25) + { + _output.WriteLine ($"Too many iterations ({iteration}): Calling Application.RequestStop."); + Application.RequestStop (); + } + } + } } diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index a293de45a..799163104 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using UICatalog; +using Xunit.Abstractions; namespace Terminal.Gui.ApplicationTests; @@ -58,6 +59,123 @@ public class KeyboardTests top.Dispose (); } + [Fact] + public void QuitKey_Default_Is_CtrlQ () + { + Application.ResetState (true); + // Before Init + Assert.Equal (Key.Empty, Application.QuitKey); + + Application.Init (new FakeDriver ()); + // After Init + Assert.Equal (Key.Q.WithCtrl, Application.QuitKey); + + Application.Shutdown(); + } + + private object _timeoutLock; + + [Fact] + public void QuitKey_Quits () + { + Assert.Null (_timeoutLock); + _timeoutLock = new object (); + + uint abortTime = 500; + bool initialized = false; + int iteration = 0; + bool shutdown = false; + object timeout = null; + + Application.InitializedChanged += OnApplicationOnInitializedChanged; + + Application.Init (new FakeDriver ()); + Assert.True (initialized); + Assert.False (shutdown); + + _output.WriteLine ("Application.Run ().Dispose ().."); + Application.Run ().Dispose (); + _output.WriteLine ("Back from Application.Run ().Dispose ()"); + + Assert.True (initialized); + Assert.False (shutdown); + + Assert.Equal (1, iteration); + + Application.Shutdown (); + + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (timeout is { }) + { + Application.RemoveTimeout (timeout); + timeout = null; + } + } + + Assert.True (initialized); + Assert.True (shutdown); + +#if DEBUG_IDISPOSABLE + Assert.Empty (Responder.Instances); +#endif + lock (_timeoutLock) + { + _timeoutLock = null; + } + + return; + + void OnApplicationOnInitializedChanged (object s, StateEventArgs a) + { + _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.NewValue); + if (a.NewValue) + { + Application.Iteration += OnApplicationOnIteration; + initialized = true; + lock (_timeoutLock) + { + timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); + } + } + else + { + Application.Iteration -= OnApplicationOnIteration; + shutdown = true; + } + } + + bool ForceCloseCallback () + { + lock (_timeoutLock) + { + _output.WriteLine ($"ForceCloseCallback. iteration: {iteration}"); + if (timeout is { }) + { + timeout = null; + } + } + Application.ResetState (true); + Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); + + return false; + } + + void OnApplicationOnIteration (object s, IterationEventArgs a) + { + _output.WriteLine ("Iteration: {0}", iteration); + iteration++; + Assert.True (iteration < 2, "Too many iterations, something is wrong."); + if (Application._initialized) + { + _output.WriteLine (" Pressing QuitKey"); + Application.OnKeyDown (Application.QuitKey); + } + } + } + [Fact] public void AlternateForwardKey_AlternateBackwardKey_Tests () { @@ -193,7 +311,7 @@ public class KeyboardTests Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); + Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); win.CanFocus = false; Assert.False (win.CanFocus); @@ -220,7 +338,7 @@ public class KeyboardTests [Fact] [AutoInitShutdown] - public void EnsuresTopOnFront_CanFocus_True_By_Keyboard_ () + public void EnsuresTopOnFront_CanFocus_True_By_Keyboard () { Toplevel top = new (); @@ -253,7 +371,7 @@ public class KeyboardTests Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); + Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); top.NewKeyDownEvent (Key.Tab.WithCtrl); Assert.True (win.CanFocus); diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 99c61e263..71e8517a0 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.IO; // Alias Console to MockConsole so we don't accidentally use Console @@ -619,16 +620,22 @@ public class MainLoopTests ); } - [Fact] - [AutoInitShutdown] - public async Task InvokeLeakTest () + [Theory] + [InlineData (typeof (FakeDriver))] + //[InlineData (typeof (NetDriver))] // BUGBUG: NetDriver never exits in this test + + //[InlineData (typeof (ANSIDriver))] + //[InlineData (typeof (WindowsDriver))] // BUGBUG: NetDriver never exits in this test + //[InlineData (typeof (CursesDriver))] // BUGBUG: CursesDriver never exits in this test + public async Task InvokeLeakTest (Type driverType) { + Application.Init (driverName: driverType.Name); Random r = new (); TextField tf = new (); var top = new Toplevel (); top.Add (tf); - const int numPasses = 5; + const int numPasses = 2; const int numIncrements = 500; const int pollMs = 2500; @@ -641,10 +648,10 @@ public class MainLoopTests Assert.Equal (numIncrements * numPasses, tbCounter); top.Dispose (); + Application.Shutdown (); } [Theory] - [AutoInitShutdown] [MemberData (nameof (TestAddIdle))] public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions ( Action action, @@ -658,6 +665,9 @@ public class MainLoopTests int pfour ) { + // TODO: Expand this test to test all drivers + Application.Init (new FakeDriver()); + total = 0; btn = null; clickMe = pclickMe; @@ -720,6 +730,8 @@ public class MainLoopTests Assert.True (taskCompleted); Assert.Equal (clickMe, btn.Text); Assert.Equal (four, total); + + Application.Shutdown (); } [Fact] diff --git a/UnitTests/Application/SynchronizatonContextTests.cs b/UnitTests/Application/SynchronizatonContextTests.cs index 3e7635580..f0dd036d3 100644 --- a/UnitTests/Application/SynchronizatonContextTests.cs +++ b/UnitTests/Application/SynchronizatonContextTests.cs @@ -5,9 +5,9 @@ namespace Terminal.Gui.ApplicationTests; public class SyncrhonizationContextTests { [Fact] - [AutoInitShutdown] public void SynchronizationContext_CreateCopy () { + Application.Init (); SynchronizationContext context = SynchronizationContext.Current; Assert.NotNull (context); @@ -15,12 +15,17 @@ public class SyncrhonizationContextTests Assert.NotNull (contextCopy); Assert.NotEqual (context, contextCopy); + Application.Shutdown (); } - [Fact] - [AutoInitShutdown] - public void SynchronizationContext_Post () + [Theory] + [InlineData (typeof (FakeDriver))] + //[InlineData (typeof (NetDriver))] + [InlineData (typeof (WindowsDriver))] + //[InlineData (typeof (CursesDriver))] + public void SynchronizationContext_Post (Type driverType) { + Application.Init (driverName: driverType.Name); SynchronizationContext context = SynchronizationContext.Current; var success = false; @@ -48,12 +53,14 @@ public class SyncrhonizationContextTests // blocks here until the RequestStop is processed at the end of the test Application.Run ().Dispose (); Assert.True (success); + Application.Shutdown (); } [Fact] [AutoInitShutdown] public void SynchronizationContext_Send () { + Application.Init (); SynchronizationContext context = SynchronizationContext.Current; var success = false; @@ -81,5 +88,6 @@ public class SyncrhonizationContextTests // blocks here until the RequestStop is processed at the end of the test Application.Run ().Dispose (); Assert.True (success); + Application.Shutdown (); } } diff --git a/UnitTests/Configuration/ConfigurationMangerTests.cs b/UnitTests/Configuration/ConfigurationMangerTests.cs index 553df407d..b1b7f2150 100644 --- a/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text.Json; +using Xunit.Abstractions; using static Terminal.Gui.ConfigurationManager; #pragma warning disable IDE1006 @@ -7,6 +8,13 @@ namespace Terminal.Gui.ConfigurationTests; public class ConfigurationManagerTests { + private readonly ITestOutputHelper _output; + + public ConfigurationManagerTests (ITestOutputHelper output) + { + _output = output; + } + public static readonly JsonSerializerOptions _jsonOptions = new () { Converters = { new AttributeJsonConverter (), new ColorJsonConverter () } @@ -402,6 +410,7 @@ public class ConfigurationManagerTests // Application is a static class PropertyInfo pi = typeof (Application).GetProperty ("QuitKey"); Assert.Equal (pi, Settings ["Application.QuitKey"].PropertyInfo); + // FrameView is not a static class and DefaultBorderStyle is Scope.Scheme pi = typeof (FrameView).GetProperty ("DefaultBorderStyle"); diff --git a/UnitTests/Drawing/AlignerTests.cs b/UnitTests/Drawing/AlignerTests.cs index cc9d1c92b..ecbe91c58 100644 --- a/UnitTests/Drawing/AlignerTests.cs +++ b/UnitTests/Drawing/AlignerTests.cs @@ -218,7 +218,7 @@ public class AlignerTests (ITestOutputHelper output) [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3 }, 9, new [] { 0, 2, 6 })] [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3 }, 10, new [] { 0, 2, 7 })] [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3 }, 11, new [] { 0, 2, 8 })] - [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3 }, 5, new [] { -1, 0, 2 })] // 5 is too small to fit the items. The first item is at -1.})] + [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3 }, 5, new [] { 0, 1, 2 })] [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3, 4 }, 10, new [] { 0, 1, 3, 6 })] [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 1, 2, 3, 4 }, 11, new [] { 0, 2, 4, 7 })] [InlineData (Alignment.Start, AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems | AlignmentModes.IgnoreFirstOrLast, new [] { 3, 3, 3 }, 21, new [] { 0, 4, 18 })] diff --git a/UnitTests/Input/EscSeqUtilsTests.cs b/UnitTests/Input/EscSeqUtilsTests.cs index f947f2bb4..3ac8e4c60 100644 --- a/UnitTests/Input/EscSeqUtilsTests.cs +++ b/UnitTests/Input/EscSeqUtilsTests.cs @@ -968,32 +968,45 @@ public class EscSeqUtilsTests public void GetConsoleKey_Tests () { ConsoleModifiers mod = 0; - Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('A', "", ref mod)); - Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('B', "", ref mod)); - Assert.Equal (_key = ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('C', "", ref mod)); - Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('D', "", ref mod)); - Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('F', "", ref mod)); - Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('H', "", ref mod)); - Assert.Equal (ConsoleKey.F1, EscSeqUtils.GetConsoleKey ('P', "", ref mod)); - Assert.Equal (ConsoleKey.F2, EscSeqUtils.GetConsoleKey ('Q', "", ref mod)); - Assert.Equal (ConsoleKey.F3, EscSeqUtils.GetConsoleKey ('R', "", ref mod)); - Assert.Equal (ConsoleKey.F4, EscSeqUtils.GetConsoleKey ('S', "", ref mod)); - Assert.Equal (ConsoleKey.Tab, EscSeqUtils.GetConsoleKey ('Z', "", ref mod)); + char keyChar = '\0'; + Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('A', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('B', "", ref mod, ref keyChar)); + Assert.Equal (_key = ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('C', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('D', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('F', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('H', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F1, EscSeqUtils.GetConsoleKey ('P', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F2, EscSeqUtils.GetConsoleKey ('Q', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F3, EscSeqUtils.GetConsoleKey ('R', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F4, EscSeqUtils.GetConsoleKey ('S', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Tab, EscSeqUtils.GetConsoleKey ('Z', "", ref mod, ref keyChar)); Assert.Equal (ConsoleModifiers.Shift, mod); - Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('\0', "", ref mod)); - Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('~', "2", ref mod)); - Assert.Equal (ConsoleKey.Delete, EscSeqUtils.GetConsoleKey ('~', "3", ref mod)); - Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('~', "5", ref mod)); - Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('~', "6", ref mod)); - Assert.Equal (ConsoleKey.F5, EscSeqUtils.GetConsoleKey ('~', "15", ref mod)); - Assert.Equal (ConsoleKey.F6, EscSeqUtils.GetConsoleKey ('~', "17", ref mod)); - Assert.Equal (ConsoleKey.F7, EscSeqUtils.GetConsoleKey ('~', "18", ref mod)); - Assert.Equal (ConsoleKey.F8, EscSeqUtils.GetConsoleKey ('~', "19", ref mod)); - Assert.Equal (ConsoleKey.F9, EscSeqUtils.GetConsoleKey ('~', "20", ref mod)); - Assert.Equal (ConsoleKey.F10, EscSeqUtils.GetConsoleKey ('~', "21", ref mod)); - Assert.Equal (ConsoleKey.F11, EscSeqUtils.GetConsoleKey ('~', "23", ref mod)); - Assert.Equal (ConsoleKey.F12, EscSeqUtils.GetConsoleKey ('~', "24", ref mod)); - Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('~', "", ref mod)); + Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('\0', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('~', "2", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Delete, EscSeqUtils.GetConsoleKey ('~', "3", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('~', "5", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('~', "6", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F5, EscSeqUtils.GetConsoleKey ('~', "15", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F6, EscSeqUtils.GetConsoleKey ('~', "17", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F7, EscSeqUtils.GetConsoleKey ('~', "18", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F8, EscSeqUtils.GetConsoleKey ('~', "19", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F9, EscSeqUtils.GetConsoleKey ('~', "20", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F10, EscSeqUtils.GetConsoleKey ('~', "21", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F11, EscSeqUtils.GetConsoleKey ('~', "23", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.F12, EscSeqUtils.GetConsoleKey ('~', "24", ref mod, ref keyChar)); + Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('~', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Add, EscSeqUtils.GetConsoleKey ('l', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Subtract, EscSeqUtils.GetConsoleKey ('m', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('p', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('q', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('r', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('s', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('t', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Clear, EscSeqUtils.GetConsoleKey ('u', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('v', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('w', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('x', "", ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('y', "", ref mod, ref keyChar)); } [Fact] diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index 9e151878e..af6560799 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -23,6 +23,7 @@ public class ScenarioTests : TestsAllViews .Where (type => type.IsClass && !type.IsAbstract && type.IsSubclassOf (typeof (Scenario))) .Select (type => new object [] { type }); + private object _timeoutLock; /// /// This runs through all Scenarios defined in UI Catalog, calling Init, Setup, and Run. @@ -30,60 +31,107 @@ public class ScenarioTests : TestsAllViews /// [Theory] [MemberData (nameof (AllScenarioTypes))] - public void Run_All_Scenarios (Type scenarioType) + public void All_Scenarios_Quit_And_Init_Shutdown_Properly (Type scenarioType) { - _output.WriteLine ($"Running Scenario '{scenarioType}'"); + Assert.Null (_timeoutLock); + _timeoutLock = new object (); + // If a previous test failed, this will ensure that the Application is in a clean state + Application.ResetState (true); + + _output.WriteLine ($"Running Scenario '{scenarioType}'"); Scenario scenario = (Scenario)Activator.CreateInstance (scenarioType); - Application.Init (new FakeDriver ()); + uint abortTime = 1500; + bool initialized = false; + bool shutdown = false; + object timeout = null; - // Press QuitKey - Assert.Empty (FakeConsole.MockKeyPresses); + Application.InitializedChanged += OnApplicationOnInitializedChanged; - FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey); + Application.ForceDriver = "FakeDriver"; + scenario.Main (); + scenario.Dispose (); + scenario = null; + Application.ForceDriver = string.Empty; - uint abortTime = 500; + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (timeout is { }) + { + timeout = null; + } + } + + + Assert.True (initialized); + Assert.True (shutdown); + +#if DEBUG_IDISPOSABLE + Assert.Empty (Responder.Instances); +#endif + + lock (_timeoutLock) + { + _timeoutLock = null; + } + + return; + + + void OnApplicationOnInitializedChanged (object s, StateEventArgs a) + { + if (a.NewValue) + { + Assert.Equal (Key.Q.WithCtrl, Application.QuitKey); + + Application.Iteration += OnApplicationOnIteration; + initialized = true; + lock (_timeoutLock) + { + timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); + } + _output.WriteLine ($"Initialized '{Application.Driver}'"); + //Dictionary> bindings = Application.GetKeyBindings (); + //Assert.NotEmpty (bindings); + //_output.WriteLine ($"bindings: {string.Join (",", bindings.Keys)}"); + //Assert.True (bindings.ContainsKey (Application.QuitKey)); + } + else + { + Application.Iteration -= OnApplicationOnIteration; + shutdown = true; + } + } // If the scenario doesn't close within 500ms, this will force it to quit bool ForceCloseCallback () { - if (Application.Top.Running && FakeConsole.MockKeyPresses.Count == 0) + lock (_timeoutLock) { - Application.RequestStop (); - - // See #2474 for why this is commented out - Assert.Fail ( - $"'{scenario.GetName ()}' failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); + if (timeout is { }) + { + timeout = null; + } } + Assert.Fail ( + $"'{scenario.GetName ()}' failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); + Application.ResetState (true); return false; } - //output.WriteLine ($" Add timeout to force quit after {abortTime}ms"); - _ = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); - - Application.Iteration += (s, a) => - { - // Press QuitKey - Assert.Empty (FakeConsole.MockKeyPresses); - FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey); - - //output.WriteLine ($" iteration {++iterations}"); - if (Application.Top.Running && FakeConsole.MockKeyPresses.Count == 0) - { - Application.RequestStop (); - Assert.Fail ($"'{scenario.GetName ()}' failed to Quit with {Application.QuitKey}. Force quit."); - } - }; - - scenario.Main (); - scenario.Dispose (); - - Application.Shutdown (); -#if DEBUG_IDISPOSABLE - Assert.Empty (Responder.Instances); -#endif + void OnApplicationOnIteration (object s, IterationEventArgs a) + { + if (Application._initialized) + { + // Press QuitKey + //_output.WriteLine ($"Forcing Quit with {Application.QuitKey}"); + Application.OnKeyDown (Application.QuitKey); + } + } } [Fact] diff --git a/UnitTests/View/HotKeyTests.cs b/UnitTests/View/HotKeyTests.cs index f1b3109f9..d284e9b01 100644 --- a/UnitTests/View/HotKeyTests.cs +++ b/UnitTests/View/HotKeyTests.cs @@ -81,7 +81,7 @@ public class HotKeyTests [InlineData (KeyCode.ShiftMask | KeyCode.AltMask, true)] [InlineData (KeyCode.CtrlMask, false)] [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask, false)] - public void KeyPress_Runs_Default_HotKey_Command (KeyCode mask, bool expected) + public void NewKeyDownEvent_Runs_Default_HotKey_Command (KeyCode mask, bool expected) { var view = new View { HotKeySpecifier = (Rune)'^', Title = "^Test" }; view.CanFocus = true; @@ -91,10 +91,10 @@ public class HotKeyTests } [Fact] - public void ProcessKeyDown_Ignores_KeyBindings_Out_Of_Scope_SuperView () + public void NewKeyDownEvent_Ignores_Focus_KeyBindings_SuperView () { var view = new View (); - view.KeyBindings.Add (Key.A, Command.HotKey); + view.KeyBindings.Add (Key.A, Command.HotKey); // implies KeyBindingScope.Focused - so this should not be invoked view.InvokingKeyBindings += (s, e) => { Assert.Fail (); }; var superView = new View (); @@ -105,7 +105,25 @@ public class HotKeyTests } [Fact] - public void ProcessKeyDown_Invokes_HotKey_Command_With_SuperView () + public void NewKeyDownEvent_Honors_HotKey_KeyBindings_SuperView () + { + var view = new View (); + view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); + bool invoked = false; + view.InvokingKeyBindings += (s, e) => { invoked = true; }; + + var superView = new View (); + superView.Add (view); + + var ke = Key.A; + superView.NewKeyDownEvent (ke); + + Assert.True (invoked); + } + + + [Fact] + public void NewKeyDownEvent_InNewKeyDownEventvokes_HotKey_Command_With_SuperView () { var view = new View { HotKeySpecifier = (Rune)'^', Title = "^Test" }; diff --git a/UnitTests/View/KeyboardEventTests.cs b/UnitTests/View/KeyboardEventTests.cs index 61e9de9d3..ec7eec6aa 100644 --- a/UnitTests/View/KeyboardEventTests.cs +++ b/UnitTests/View/KeyboardEventTests.cs @@ -421,7 +421,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews var view = new KeyBindingsTestView (); view.CommandReturns = toReturn; - bool? result = view.OnInvokingKeyBindings (Key.A); + bool? result = view.OnInvokingKeyBindings (Key.A, KeyBindingScope.HotKey | KeyBindingScope.Focused); Assert.Equal (expected, result); } @@ -449,9 +449,9 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews public bool OnKeyUpContinued { get; set; } public override string Text { get; set; } - 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 != null && (bool)handled) { diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs index c326e7a33..6c53aceb8 100644 --- a/UnitTests/View/NavigationTests.cs +++ b/UnitTests/View/NavigationTests.cs @@ -620,133 +620,133 @@ public class NavigationTests (ITestOutputHelper output) top1.Dispose (); } - [Fact] - [AutoInitShutdown] - public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_With_Top_KeyPress_Event () - { - var sbQuiting = false; - var tfQuiting = false; - var topQuiting = false; +// [Fact] +// [AutoInitShutdown] +// public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_With_Top_KeyPress_Event () +// { +// var sbQuiting = false; +// var tfQuiting = false; +// var topQuiting = false; - var sb = new StatusBar ( - new StatusItem [] - { - new ( - KeyCode.CtrlMask | KeyCode.Q, - "~^Q~ Quit", - () => sbQuiting = true - ) - } - ); - var tf = new TextField (); - tf.KeyDown += Tf_KeyPressed; +// var sb = new StatusBar ( +// new Shortcut [] +// { +// new ( +// KeyCode.CtrlMask | KeyCode.Q, +// "Quit", +// () => sbQuiting = true +// ) +// } +// ); +// var tf = new TextField (); +// tf.KeyDown += Tf_KeyPressed; - void Tf_KeyPressed (object sender, Key obj) - { - if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - { - obj.Handled = tfQuiting = true; - } - } +// void Tf_KeyPressed (object sender, Key obj) +// { +// if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) +// { +// obj.Handled = tfQuiting = true; +// } +// } - var win = new Window (); - win.Add (sb, tf); - Toplevel top = new (); - top.KeyDown += Top_KeyPress; +// var win = new Window (); +// win.Add (sb, tf); +// Toplevel top = new (); +// top.KeyDown += Top_KeyPress; - void Top_KeyPress (object sender, Key obj) - { - if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - { - obj.Handled = topQuiting = true; - } - } +// void Top_KeyPress (object sender, Key obj) +// { +// if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) +// { +// obj.Handled = topQuiting = true; +// } +// } - top.Add (win); - Application.Begin (top); +// top.Add (win); +// Application.Begin (top); - Assert.False (sbQuiting); - Assert.False (tfQuiting); - Assert.False (topQuiting); +// Assert.False (sbQuiting); +// Assert.False (tfQuiting); +// Assert.False (topQuiting); - Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); - Assert.False (sbQuiting); - Assert.True (tfQuiting); - Assert.False (topQuiting); +// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Assert.False (sbQuiting); +// Assert.True (tfQuiting); +// Assert.False (topQuiting); -#if BROKE_WITH_2927 - tf.KeyPressed -= Tf_KeyPress; - tfQuiting = false; - Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); - Application.MainLoop.RunIteration (); - Assert.True (sbQuiting); - Assert.False (tfQuiting); - Assert.False (topQuiting); +//#if BROKE_WITH_2927 +// tf.KeyPressed -= Tf_KeyPress; +// tfQuiting = false; +// Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); +// Application.MainLoop.RunIteration (); +// Assert.True (sbQuiting); +// Assert.False (tfQuiting); +// Assert.False (topQuiting); - sb.RemoveItem (0); - sbQuiting = false; - Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); - Application.MainLoop.RunIteration (); - Assert.False (sbQuiting); - Assert.False (tfQuiting); +// sb.RemoveItem (0); +// sbQuiting = false; +// Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); +// Application.MainLoop.RunIteration (); +// Assert.False (sbQuiting); +// Assert.False (tfQuiting); -// This test is now invalid because `win` is focused, so it will receive the keypress - Assert.True (topQuiting); -#endif - top.Dispose (); - } +//// This test is now invalid because `win` is focused, so it will receive the keypress +// Assert.True (topQuiting); +//#endif +// top.Dispose (); +// } - [Fact] - [AutoInitShutdown] - public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_Without_Top_KeyPress_Event () - { - var sbQuiting = false; - var tfQuiting = false; +// [Fact] +// [AutoInitShutdown] +// public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_Without_Top_KeyPress_Event () +// { +// var sbQuiting = false; +// var tfQuiting = false; - var sb = new StatusBar ( - new StatusItem [] - { - new ( - KeyCode.CtrlMask | KeyCode.Q, - "~^Q~ Quit", - () => sbQuiting = true - ) - } - ); - var tf = new TextField (); - tf.KeyDown += Tf_KeyPressed; +// var sb = new StatusBar ( +// new Shortcut [] +// { +// new ( +// KeyCode.CtrlMask | KeyCode.Q, +// "~^Q~ Quit", +// () => sbQuiting = true +// ) +// } +// ); +// var tf = new TextField (); +// tf.KeyDown += Tf_KeyPressed; - void Tf_KeyPressed (object sender, Key obj) - { - if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - { - obj.Handled = tfQuiting = true; - } - } +// void Tf_KeyPressed (object sender, Key obj) +// { +// if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) +// { +// obj.Handled = tfQuiting = true; +// } +// } - var win = new Window (); - win.Add (sb, tf); - Toplevel top = new (); - top.Add (win); - Application.Begin (top); +// var win = new Window (); +// win.Add (sb, tf); +// Toplevel top = new (); +// top.Add (win); +// Application.Begin (top); - Assert.False (sbQuiting); - Assert.False (tfQuiting); +// Assert.False (sbQuiting); +// Assert.False (tfQuiting); - Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); - Assert.False (sbQuiting); - Assert.True (tfQuiting); +// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Assert.False (sbQuiting); +// Assert.True (tfQuiting); - tf.KeyDown -= Tf_KeyPressed; - tfQuiting = false; - Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); - Application.MainLoop.RunIteration (); -#if BROKE_WITH_2927 - Assert.True (sbQuiting); - Assert.False (tfQuiting); -#endif - top.Dispose (); - } +// tf.KeyDown -= Tf_KeyPressed; +// tfQuiting = false; +// Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); +// Application.MainLoop.RunIteration (); +//#if BROKE_WITH_2927 +// Assert.True (sbQuiting); +// Assert.False (tfQuiting); +//#endif +// top.Dispose (); +// } [Fact] [SetupFakeDriver] @@ -1535,7 +1535,6 @@ public class NavigationTests (ITestOutputHelper output) } [Fact] - [AutoInitShutdown] public void WindowDispose_CanFocusProblem () { // Arrange diff --git a/UnitTests/Views/BarTests.cs b/UnitTests/Views/BarTests.cs new file mode 100644 index 000000000..d5e50c7dc --- /dev/null +++ b/UnitTests/Views/BarTests.cs @@ -0,0 +1,105 @@ +using JetBrains.Annotations; + +namespace Terminal.Gui.ViewsTests; + +[TestSubject (typeof (Bar))] +public class BarTests +{ + [Fact] + public void Constructor_Defaults () + { + var bar = new Bar (); + + Assert.NotNull (bar); + Assert.True (bar.CanFocus); + Assert.IsType (bar.Width); + Assert.IsType (bar.Height); + + // TOOD: more + } + + [Fact] + public void Constructor_InitializesEmpty_WhenNoShortcutsProvided () + { + var bar = new Bar (); + Assert.Empty (bar.Subviews); + } + + [Fact] + public void Constructor_InitializesWithShortcuts_WhenProvided () + { + var shortcuts = new List + { + new Shortcut(Key.Empty, "Command1", null, null), + new Shortcut(Key.Empty, "Command2", null, null) + }; + + var bar = new Bar (shortcuts); + + Assert.Equal (shortcuts.Count, bar.Subviews.Count); + for (int i = 0; i < shortcuts.Count; i++) + { + Assert.Same (shortcuts [i], bar.Subviews [i]); + } + } + + [Fact] + public void OrientationProperty_SetsCorrectly () + { + var bar = new Bar (); + Assert.Equal (Orientation.Horizontal, bar.Orientation); // Default value + + bar.Orientation = Orientation.Vertical; + Assert.Equal (Orientation.Vertical, bar.Orientation); + } + + [Fact] + public void AlignmentModesProperty_SetsCorrectly () + { + var bar = new Bar (); + Assert.Equal (AlignmentModes.StartToEnd, bar.AlignmentModes); // Default value + + bar.AlignmentModes = AlignmentModes.EndToStart; + Assert.Equal (AlignmentModes.EndToStart, bar.AlignmentModes); + } + + [Fact] + public void AddShortcutAt_InsertsShortcutCorrectly () + { + var bar = new Bar (); + var shortcut = new Shortcut (Key.Empty, "Command", null, null); + bar.AddShortcutAt (0, shortcut); + + Assert.Contains (shortcut, bar.Subviews); + } + + [Fact] + public void RemoveShortcut_RemovesShortcutCorrectly () + { + var shortcut1 = new Shortcut (Key.Empty, "Command1", null, null); + var shortcut2 = new Shortcut (Key.Empty, "Command2", null, null); + var bar = new Bar (new List { shortcut1, shortcut2 }); + + var removedShortcut = bar.RemoveShortcut (0); + + Assert.Same (shortcut1, removedShortcut); + Assert.DoesNotContain (shortcut1, bar.Subviews); + Assert.Contains (shortcut2, bar.Subviews); + } + + [Fact] + public void Layout_ChangesBasedOnOrientation () + { + var shortcut1 = new Shortcut (Key.Empty, "Command1", null, null); + var shortcut2 = new Shortcut (Key.Empty, "Command2", null, null); + var bar = new Bar (new List { shortcut1, shortcut2 }); + + bar.Orientation = Orientation.Horizontal; + bar.LayoutSubviews (); + // TODO: Assert specific layout expectations for horizontal orientation + + bar.Orientation = Orientation.Vertical; + bar.LayoutSubviews (); + // TODO: Assert specific layout expectations for vertical orientation + } +} diff --git a/UnitTests/Views/ButtonTests.cs b/UnitTests/Views/ButtonTests.cs index d4d9c9cd0..03f0dcba0 100644 --- a/UnitTests/Views/ButtonTests.cs +++ b/UnitTests/Views/ButtonTests.cs @@ -258,6 +258,93 @@ public class ButtonTests (ITestOutputHelper output) Assert.True (clicked); } + [Theory] + [InlineData (false, 0)] + [InlineData (true, 1)] + public void Space_Fires_Accept (bool focused, int expected) + { + View superView = new View () + { + CanFocus = true, + }; + + Button button = new (); + + button.CanFocus = focused; + + int acceptInvoked = 0; + button.Accept += (s, e) => acceptInvoked++; + + superView.Add (button); + button.SetFocus (); + Assert.Equal (focused, button.HasFocus); + + superView.NewKeyDownEvent (Key.Space); + + Assert.Equal (expected, acceptInvoked); + + superView.Dispose (); + } + + [Theory] + [InlineData (false, 0)] + [InlineData (true, 1)] + public void Enter_Fires_Accept (bool focused, int expected) + { + View superView = new View () + { + CanFocus = true, + }; + + Button button = new (); + + button.CanFocus = focused; + + int acceptInvoked = 0; + button.Accept += (s, e) => acceptInvoked++; + + superView.Add (button); + button.SetFocus (); + Assert.Equal (focused, button.HasFocus); + + superView.NewKeyDownEvent (Key.Enter); + + Assert.Equal (expected, acceptInvoked); + + superView.Dispose (); + } + + [Theory] + [InlineData (false, 1)] + [InlineData (true, 1)] + public void HotKey_Fires_Accept (bool focused, int expected) + { + View superView = new View () + { + CanFocus = true, + }; + + Button button = new () + { + HotKey = Key.A + }; + + button.CanFocus = focused; + + int acceptInvoked = 0; + button.Accept += (s, e) => acceptInvoked++; + + superView.Add (button); + button.SetFocus (); + Assert.Equal (focused, button.HasFocus); + + superView.NewKeyDownEvent (Key.A); + + Assert.Equal (expected, acceptInvoked); + + superView.Dispose (); + } + /// /// This test demonstrates how to change the activation key for Button as described in the README.md keyboard /// handling section @@ -279,7 +366,9 @@ public class ButtonTests (ITestOutputHelper output) top.Add (btn); Application.Begin (top); - // default keybinding is Space which results in keypress + Assert.True (btn.HasFocus); + + // default keybinding is Space which results in Command.Accept (when focused) Application.OnKeyDown (new ((KeyCode)' ')); Assert.Equal (1, pressed); @@ -292,8 +381,7 @@ public class ButtonTests (ITestOutputHelper output) Assert.Equal (1, pressed); // Set a new binding of b for the click (Accept) event - btn.KeyBindings.Add (Key.B, Command.HotKey); - btn.KeyBindings.Add (Key.B, Command.Accept); + btn.KeyBindings.Add (Key.B, Command.HotKey); // b will now trigger the Accept command (when focused or not) // now pressing B should call the button click event Application.OnKeyDown (Key.B); diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index ae697c7f7..6d42add38 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -113,138 +113,6 @@ public class ContextMenuTests (ITestOutputHelper output) top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void ContextMenu_On_Toplevel_With_A_MenuBar_TextField_StatusBar () - { - Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); - - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ("File", "", null), - new MenuBarItem ("Edit", "", null) - ] - }; - - var label = new Label { X = 2, Y = 3, Text = "Label:" }; - - var tf = new TextField { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = 20, Text = "TextField" }; - - var statusBar = new StatusBar ( - [ - new StatusItem (KeyCode.F1, "~F1~ Help", null), - new StatusItem (KeyCode.CtrlMask | KeyCode.Q, "~^Q~ Quit", null) - ] - ); - - var top = new Toplevel (); - top.Add (menu, label, tf, statusBar); - ((FakeDriver)Application.Driver).SetBufferSize (45, 17); - Application.Begin (top); - - Assert.Equal (new Rectangle (9, 3, 20, 1), tf.Frame); - Assert.True (tf.HasFocus); - - tf.ContextMenu.Show (); - Assert.True (ContextMenu.IsShow); - Assert.Equal (new Point (9, 3), tf.ContextMenu.Position); - Application.Top.Draw (); - - var expected = @" - File Edit - - - Label: TextField - ┌─────────────────────┐ - │ Select All Ctrl+T │ - │ Delete All Ctrl+R │ - │ Copy Ctrl+C │ - │ Cut Ctrl+X │ - │ Paste Ctrl+V │ - │ Undo Ctrl+Z │ - │ Redo Ctrl+Y │ - └─────────────────────┘ - - - - F1 Help │ ^Q Quit -"; - - Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (1, 0, 32, 17), pos); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void ContextMenu_On_Toplevel_With_A_MenuBar_Window_TextField_StatusBar () - { - Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); - - var menu = new MenuBar - { - Menus = - [ - new MenuBarItem ("File", "", null), - new MenuBarItem ("Edit", "", null) - ] - }; - - var label = new Label { X = 2, Y = 3, Text = "Label:" }; - - var tf = new TextField { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = 20, Text = "TextField" }; - - var win = new Window (); - win.Add (label, tf); - - var statusBar = new StatusBar ( - new [] - { - new StatusItem (KeyCode.F1, "~F1~ Help", null), - new StatusItem (KeyCode.CtrlMask | KeyCode.Q, "~^Q~ Quit", null) - } - ); - - var top = new Toplevel (); - top.Add (menu, win, statusBar); - Application.Begin (top); - ((FakeDriver)Application.Driver).SetBufferSize (44, 17); - - Assert.Equal (new Rectangle (9, 3, 20, 1), tf.Frame); - Assert.True (tf.HasFocus); - - tf.ContextMenu.Show (); - Assert.True (ContextMenu.IsShow); - Assert.Equal (new Point (10, 5), tf.ContextMenu.Position); - Application.Top.Draw (); - - var expected = @" - File Edit -┌──────────────────────────────────────────┐ -│ │ -│ │ -│ │ -│ Label: TextField │ -│ ┌─────────────────────┐ │ -│ │ Select All Ctrl+T │ │ -│ │ Delete All Ctrl+R │ │ -│ │ Copy Ctrl+C │ │ -│ │ Cut Ctrl+X │ │ -│ │ Paste Ctrl+V │ │ -│ │ Undo Ctrl+Z │ │ -│ │ Redo Ctrl+Y │ │ -│ └─────────────────────┘ │ -└──────────────────────────────────────────┘ - F1 Help │ ^Q Quit -"; - - Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (1, 0, 44, 17), pos); - top.Dispose (); - } - [Fact] [AutoInitShutdown] public void Draw_A_ContextMenu_Over_A_Borderless_Top () diff --git a/UnitTests/Views/LabelTests.cs b/UnitTests/Views/LabelTests.cs index 6c85de076..7c04bcbe6 100644 --- a/UnitTests/Views/LabelTests.cs +++ b/UnitTests/Views/LabelTests.cs @@ -907,69 +907,6 @@ e top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void AnchorEnd_Better_Than_Bottom_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel () - { - var win = new Window (); - - // Label is AutoSize == true - var label = new Label - { - Text = "This should be the last line.", - ColorScheme = Colors.ColorSchemes ["Menu"], - - X = 0, - Y = Pos.AnchorEnd (1) - }; - - win.Add (label); - - var menu = new MenuBar { Menus = new MenuBarItem [] { new ("Menu", "", null) } }; - var status = new StatusBar (new StatusItem [] { new (KeyCode.F1, "~F1~ Help", null) }); - Toplevel top = new (); - top.Add (win, menu, status); - RunState rs = Application.Begin (top); - - Assert.Equal (new (0, 0, 80, 25), top.Frame); - Assert.Equal (new (0, 0, 80, 1), menu.Frame); - Assert.Equal (new (0, 24, 80, 1), status.Frame); - Assert.Equal (new (0, 1, 80, 23), win.Frame); - Assert.Equal (new (0, 20, 29, 1), label.Frame); - - var expected = @" - Menu -┌──────────────────────────────────────────────────────────────────────────────┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│This should be the last line. │ -└──────────────────────────────────────────────────────────────────────────────┘ - F1 Help -"; - - TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - Application.End (rs); - top.Dispose (); - } - // TODO: This is a Label test. Move to label tests if there's not already a test for this. [Fact] [AutoInitShutdown] @@ -1018,6 +955,7 @@ e top.Dispose (); } +#if V2_STATUSBAR // TODO: This is a Label test. Move to label tests if there's not already a test for this. [Fact] @@ -1083,7 +1021,7 @@ e Application.End (rs); top.Dispose (); } - +#endif // TODO: This is a Dim test. Move to Dim tests. [Fact] diff --git a/UnitTests/Views/ListViewTests.cs b/UnitTests/Views/ListViewTests.cs index 3f222b1dc..5f157a81d 100644 --- a/UnitTests/Views/ListViewTests.cs +++ b/UnitTests/Views/ListViewTests.cs @@ -679,6 +679,9 @@ Item 6", public int Count => 0; public int Length => 0; + + public bool SuspendCollectionChangedEvent { get => throw new NotImplementedException (); set => throw new NotImplementedException (); } + public bool IsMarked (int item) { throw new NotImplementedException (); } public void Render ( @@ -1075,4 +1078,82 @@ Item 6", } } } + + [Fact] + public void ListWrapper_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () + { + var added = 0; + ObservableCollection source = []; + ListWrapper lw = new (source); + + lw.CollectionChanged += Lw_CollectionChanged; + + lw.SuspendCollectionChangedEvent = true; + + for (int i = 0; i < 3; i++) + { + source.Add ($"Item{i}"); + } + Assert.Equal (0, added); + Assert.Equal (3, lw.Count); + Assert.Equal (3, source.Count); + + lw.SuspendCollectionChangedEvent = false; + + for (int i = 3; i < 6; i++) + { + source.Add ($"Item{i}"); + } + Assert.Equal (3, added); + Assert.Equal (6, lw.Count); + Assert.Equal (6, source.Count); + + + void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + } + } + + [Fact] + public void ListView_SuspendCollectionChangedEvent_ResumeSuspendCollectionChangedEvent_Tests () + { + var added = 0; + ObservableCollection source = []; + ListView lv = new ListView { Source = new ListWrapper (source) }; + + lv.CollectionChanged += Lw_CollectionChanged; + + lv.SuspendCollectionChangedEvent (); + + for (int i = 0; i < 3; i++) + { + source.Add ($"Item{i}"); + } + Assert.Equal (0, added); + Assert.Equal (3, lv.Source.Count); + Assert.Equal (3, source.Count); + + lv.ResumeSuspendCollectionChangedEvent (); + + for (int i = 3; i < 6; i++) + { + source.Add ($"Item{i}"); + } + Assert.Equal (3, added); + Assert.Equal (6, lv.Source.Count); + Assert.Equal (6, source.Count); + + + void Lw_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + added++; + } + } + } } \ No newline at end of file diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index faf0275f2..4ddf68321 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -73,6 +73,7 @@ public class RadioGroupTests (ITestOutputHelper output) public void KeyBindings_Command () { var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } }; + rg.SetFocus(); Assert.True (rg.NewKeyDownEvent (Key.CursorUp)); Assert.True (rg.NewKeyDownEvent (Key.CursorDown)); diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs new file mode 100644 index 000000000..8cd448787 --- /dev/null +++ b/UnitTests/Views/ShortcutTests.cs @@ -0,0 +1,317 @@ +using JetBrains.Annotations; + +namespace Terminal.Gui.ViewsTests; + +[TestSubject (typeof (Shortcut))] +public class ShortcutTests +{ + [Fact] + public void Constructor_Defaults () + { + var shortcut = new Shortcut (); + + Assert.NotNull (shortcut); + Assert.True (shortcut.CanFocus); + Assert.IsType (shortcut.Width); + Assert.IsType (shortcut.Height); + + // TOOD: more + } + + [Theory] + [InlineData ("", "", KeyCode.Null, 2)] + [InlineData ("C", "", KeyCode.Null, 3)] + [InlineData ("", "H", KeyCode.Null, 5)] + [InlineData ("", "", KeyCode.K, 5)] + [InlineData ("C", "", KeyCode.K, 6)] + [InlineData ("C", "H", KeyCode.Null, 6)] + [InlineData ("", "H", KeyCode.K, 8)] + [InlineData ("C", "H", KeyCode.K, 9)] + public void NaturalSize (string command, string help, Key key, int expectedWidth) + { + var shortcut = new Shortcut + { + Title = command, + HelpText = help, + Key = key + }; + + Assert.IsType (shortcut.Width); + Assert.IsType (shortcut.Height); + + shortcut.LayoutSubviews (); + shortcut.SetRelativeLayout (new (100, 100)); + + // |0123456789 + // | C H K | + Assert.Equal (expectedWidth, shortcut.Frame.Width); + } + + [Theory] + [InlineData (5, 0, 3, 6)] + [InlineData (6, 0, 3, 6)] + [InlineData (7, 0, 3, 6)] + [InlineData (8, 0, 3, 6)] + [InlineData (9, 0, 3, 6)] + [InlineData (10, 0, 4, 7)] + [InlineData (11, 0, 5, 8)] + public void Set_Width_Layouts_Correctly (int width, int expectedCmdX, int expectedHelpX, int expectedKeyX) + { + var shortcut = new Shortcut + { + Width = width, + Title = "C", + Text = "H", + Key = Key.K + }; + + shortcut.LayoutSubviews (); + shortcut.SetRelativeLayout (new (100, 100)); + + // 0123456789 + // -C--H--K- + Assert.Equal (expectedCmdX, shortcut.CommandView.Frame.X); + Assert.Equal (expectedHelpX, shortcut.HelpView.Frame.X); + Assert.Equal (expectedKeyX, shortcut.KeyView.Frame.X); + } + + [Fact] + public void CommandView_Text_And_Title_Track () + { + var shortcut = new Shortcut + { + Title = "T" + }; + + Assert.Equal (shortcut.Title, shortcut.CommandView.Text); + + shortcut = new (); + + shortcut.CommandView = new() + { + Text = "T" + }; + Assert.Equal (shortcut.Title, shortcut.CommandView.Text); + } + + [Fact] + public void HelpText_And_Text_Are_The_Same () + { + var shortcut = new Shortcut + { + Text = "H" + }; + + Assert.Equal (shortcut.Text, shortcut.HelpText); + + shortcut = new() + { + HelpText = "H" + }; + + Assert.Equal (shortcut.Text, shortcut.HelpText); + } + + [Theory] + [InlineData (KeyCode.Null, "")] + [InlineData (KeyCode.F1, "F1")] + public void KeyView_Text_Tracks_Key (Key key, string expected) + { + var shortcut = new Shortcut + { + Key = key + }; + + Assert.Equal (expected, shortcut.KeyView.Text); + } + + // Test Key + [Fact] + public void Key_Defaults_To_Empty () + { + var shortcut = new Shortcut (); + + Assert.Equal (Key.Empty, shortcut.Key); + } + + [Fact] + public void Key_Can_Be_Set () + { + var shortcut = new Shortcut (); + + shortcut.Key = Key.F1; + + Assert.Equal (Key.F1, shortcut.Key); + } + + [Fact] + public void Key_Can_Be_Set_To_Empty () + { + var shortcut = new Shortcut (); + + shortcut.Key = Key.Empty; + + Assert.Equal (Key.Empty, shortcut.Key); + } + + // Test KeyBindingScope + + // Test Key gets bound correctly + [Fact] + public void KeyBindingScope_Defaults_To_HotKey () + { + var shortcut = new Shortcut (); + + Assert.Equal (KeyBindingScope.HotKey, shortcut.KeyBindingScope); + } + + [Fact] + public void KeyBindingScope_Can_Be_Set () + { + var shortcut = new Shortcut (); + + shortcut.KeyBindingScope = KeyBindingScope.Application; + + Assert.Equal (KeyBindingScope.Application, shortcut.KeyBindingScope); + } + + [Fact] + public void Setting_Key_Binds_Key_To_CommandView_Accept () + { + var shortcut = new Shortcut (); + + shortcut.Key = Key.F1; + + // TODO: + } + + [Theory] + [InlineData (Orientation.Horizontal)] + [InlineData (Orientation.Vertical)] + public void Orientation_SetsCorrectly (Orientation orientation) + { + var shortcut = new Shortcut + { + Orientation = orientation + }; + + Assert.Equal (orientation, shortcut.Orientation); + } + + [Theory] + [InlineData (AlignmentModes.StartToEnd)] + [InlineData (AlignmentModes.EndToStart)] + public void AlignmentModes_SetsCorrectly (AlignmentModes alignmentModes) + { + var shortcut = new Shortcut + { + AlignmentModes = alignmentModes + }; + + Assert.Equal (alignmentModes, shortcut.AlignmentModes); + } + + [Fact] + public void Action_SetsAndGetsCorrectly () + { + var actionInvoked = false; + + var shortcut = new Shortcut + { + Action = () => { actionInvoked = true; } + }; + + shortcut.Action.Invoke (); + + Assert.True (actionInvoked); + } + + [Fact] + public void ColorScheme_SetsAndGetsCorrectly () + { + var colorScheme = new ColorScheme (); + + var shortcut = new Shortcut + { + ColorScheme = colorScheme + }; + + Assert.Same (colorScheme, shortcut.ColorScheme); + } + + [Fact] + public void Subview_Visibility_Controlled_By_Removal () + { + var shortcut = new Shortcut (); + + Assert.True (shortcut.CommandView.Visible); + Assert.Contains (shortcut.CommandView, shortcut.Subviews); + Assert.True (shortcut.HelpView.Visible); + Assert.DoesNotContain (shortcut.HelpView, shortcut.Subviews); + Assert.True (shortcut.KeyView.Visible); + Assert.DoesNotContain (shortcut.KeyView, shortcut.Subviews); + + shortcut.HelpText = "help"; + Assert.True (shortcut.HelpView.Visible); + Assert.Contains (shortcut.HelpView, shortcut.Subviews); + Assert.True (shortcut.KeyView.Visible); + Assert.DoesNotContain (shortcut.KeyView, shortcut.Subviews); + + shortcut.Key = Key.A; + Assert.True (shortcut.HelpView.Visible); + Assert.Contains (shortcut.HelpView, shortcut.Subviews); + Assert.True (shortcut.KeyView.Visible); + Assert.Contains (shortcut.KeyView, shortcut.Subviews); + + shortcut.HelpView.Visible = false; + shortcut.ShowHide (); + Assert.False (shortcut.HelpView.Visible); + Assert.DoesNotContain (shortcut.HelpView, shortcut.Subviews); + Assert.True (shortcut.KeyView.Visible); + Assert.Contains (shortcut.KeyView, shortcut.Subviews); + + shortcut.KeyView.Visible = false; + shortcut.ShowHide (); + Assert.False (shortcut.HelpView.Visible); + Assert.DoesNotContain (shortcut.HelpView, shortcut.Subviews); + Assert.False (shortcut.KeyView.Visible); + Assert.DoesNotContain (shortcut.KeyView, shortcut.Subviews); + } + + [Fact] + public void Focus_CanFocus_Default_Is_True () + { + Shortcut shortcut = new (); + shortcut.Key = Key.A; + shortcut.Text = "Help"; + shortcut.Title = "Command"; + Assert.True (shortcut.CanFocus); + Assert.False (shortcut.CommandView.CanFocus); + } + + [Fact] + public void Focus_CanFocus_CommandView_Add_Tracks () + { + Shortcut shortcut = new (); + Assert.True (shortcut.CanFocus); + Assert.False (shortcut.CommandView.CanFocus); + + shortcut.CommandView = new () { CanFocus = true }; + Assert.False (shortcut.CommandView.CanFocus); + + shortcut.CommandView.CanFocus = true; + Assert.True (shortcut.CommandView.CanFocus); + + shortcut.CanFocus = false; + Assert.False (shortcut.CanFocus); + Assert.True (shortcut.CommandView.CanFocus); + + shortcut.CommandView.CanFocus = false; + Assert.False (shortcut.CanFocus); + Assert.False (shortcut.CommandView.CanFocus); + + shortcut.CommandView.CanFocus = true; + Assert.False (shortcut.CanFocus); + Assert.True (shortcut.CommandView.CanFocus); + } +} diff --git a/UnitTests/Views/SliderTests.cs b/UnitTests/Views/SliderTests.cs index 3f1298726..eaab6ba69 100644 --- a/UnitTests/Views/SliderTests.cs +++ b/UnitTests/Views/SliderTests.cs @@ -310,7 +310,7 @@ public class SliderTests // Act slider.FocusedOption = 2; - bool result = slider.Set (); + bool result = slider.Select (); // Assert Assert.True (result); diff --git a/UnitTests/Views/StatusBarTests.cs b/UnitTests/Views/StatusBarTests.cs index 9de049845..ce404f7b5 100644 --- a/UnitTests/Views/StatusBarTests.cs +++ b/UnitTests/Views/StatusBarTests.cs @@ -1,137 +1,97 @@ using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; - public class StatusBarTests (ITestOutputHelper output) { [Fact] public void AddItemAt_RemoveItem_Replacing () { - var sb = new StatusBar ( - new StatusItem [] - { - new (KeyCode.CtrlMask | KeyCode.Q, "~^O~ Open", null), - new (KeyCode.CtrlMask | KeyCode.Q, "~^S~ Save", null), - new (KeyCode.CtrlMask | KeyCode.Q, "~^Q~ Quit", null) - } + var sb = new StatusBar ([ + new (Key.O.WithCtrl, "Open", null), + new (Key.S.WithCtrl, "Save", null), + new (Key.Q.WithCtrl, "Quit", null) + ] ); - sb.AddItemAt (2, new (KeyCode.CtrlMask | KeyCode.Q, "~^C~ Close", null)); + sb.AddShortcutAt (2, new (Key.C.WithCtrl, "Close", null)); - Assert.Equal ("~^O~ Open", sb.Items [0].Title); - Assert.Equal ("~^S~ Save", sb.Items [1].Title); - Assert.Equal ("~^C~ Close", sb.Items [2].Title); - Assert.Equal ("~^Q~ Quit", sb.Items [^1].Title); + Assert.Equal ("Open", sb.Subviews [0].Title); + Assert.Equal ("Save", sb.Subviews [1].Title); + Assert.Equal ("Close", sb.Subviews [2].Title); + Assert.Equal ("Quit", sb.Subviews [^1].Title); - Assert.Equal ("~^S~ Save", sb.RemoveItem (1).Title); + Assert.Equal ("Save", sb.RemoveShortcut (1).Title); - Assert.Equal ("~^O~ Open", sb.Items [0].Title); - Assert.Equal ("~^C~ Close", sb.Items [1].Title); - Assert.Equal ("~^Q~ Quit", sb.Items [^1].Title); + Assert.Equal ("Open", sb.Subviews [0].Title); + Assert.Equal ("Close", sb.Subviews [1].Title); + Assert.Equal ("Quit", sb.Subviews [^1].Title); - sb.Items [1] = new (KeyCode.CtrlMask | KeyCode.A, "~^A~ Save As", null); + sb.AddShortcutAt (1, new Shortcut (Key.A.WithCtrl, "Save As", null)); - Assert.Equal ("~^O~ Open", sb.Items [0].Title); - Assert.Equal ("~^A~ Save As", sb.Items [1].Title); - Assert.Equal ("~^Q~ Quit", sb.Items [^1].Title); + Assert.Equal ("Open", sb.Subviews [0].Title); + Assert.Equal ("Save As", sb.Subviews [1].Title); + Assert.Equal ("Quit", sb.Subviews [^1].Title); } - [Fact] - [AutoInitShutdown] - public void CanExecute_ProcessHotKey () - { - Window win = null; + //[Fact] + //[AutoInitShutdown] + //public void CanExecute_ProcessHotKey () + //{ + // Window win = null; - var statusBar = new StatusBar ( - new StatusItem [] - { - new ( - KeyCode.CtrlMask | KeyCode.N, - "~^N~ New", - New, - CanExecuteNew - ), - new ( - KeyCode.CtrlMask | KeyCode.C, - "~^C~ Close", - Close, - CanExecuteClose - ) - } - ); - Toplevel top = new (); - top.Add (statusBar); + // var statusBar = new StatusBar ( + // new Shortcut [] + // { + // new ( + // KeyCode.CtrlMask | KeyCode.N, + // "~^N~ New", + // New, + // CanExecuteNew + // ), + // new ( + // KeyCode.CtrlMask | KeyCode.C, + // "~^C~ Close", + // Close, + // CanExecuteClose + // ) + // } + // ); + // Toplevel top = new (); + // top.Add (statusBar); - bool CanExecuteNew () { return win == null; } + // bool CanExecuteNew () { return win == null; } - void New () { win = new (); } + // void New () { win = new (); } - bool CanExecuteClose () { return win != null; } + // bool CanExecuteClose () { return win != null; } - void Close () { win = null; } + // void Close () { win = null; } - Application.Begin (top); + // Application.Begin (top); - Assert.Null (win); - Assert.True (CanExecuteNew ()); - Assert.False (CanExecuteClose ()); + // Assert.Null (win); + // Assert.True (CanExecuteNew ()); + // Assert.False (CanExecuteClose ()); - Assert.True (top.NewKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop.RunIteration (); - Assert.NotNull (win); - Assert.False (CanExecuteNew ()); - Assert.True (CanExecuteClose ()); - top.Dispose (); - } + // Assert.True (top.NewKeyDownEvent (Key.N.WithCtrl)); + // Application.MainLoop.RunIteration (); + // Assert.NotNull (win); + // Assert.False (CanExecuteNew ()); + // Assert.True (CanExecuteClose ()); + // top.Dispose (); + //} [Fact] [AutoInitShutdown] public void Redraw_Output () { - var sb = new StatusBar ( - new StatusItem [] - { - new (KeyCode.CtrlMask | KeyCode.O, "~^O~ Open", null), - new (Application.QuitKey, $"{Application.QuitKey} to Quit!", null) - } - ); - var top = new Toplevel (); - top.Add (sb); - - sb.OnDrawContent (sb.Viewport); - - var expected = @$" -^O Open { - CM.Glyphs.VLine -} Ctrl+Q to Quit! -"; - TestHelpers.AssertDriverContentsAre (expected, output); - top.Dispose (); } [Fact] [AutoInitShutdown] public void Redraw_Output_CTRLQ () { - var sb = new StatusBar ( - new StatusItem [] - { - new (KeyCode.CtrlMask | KeyCode.O, "~CTRL-O~ Open", null), - new (KeyCode.CtrlMask | KeyCode.Q, "~CTRL-Q~ Quit", null) - } - ); - var top = new Toplevel (); - top.Add (sb); - sb.OnDrawContent (sb.Viewport); - var expected = @$" -CTRL-O Open { - CM.Glyphs.VLine -} CTRL-Q Quit -"; - - TestHelpers.AssertDriverContentsAre (expected, output); - top.Dispose (); } [Fact] @@ -141,11 +101,11 @@ CTRL-O Open { var msg = ""; var sb = new StatusBar ( - new StatusItem [] + new Shortcut [] { new ( Application.QuitKey, - $"{Application.QuitKey} to Quit", + $"Quit", () => msg = "Quiting..." ) } @@ -157,13 +117,13 @@ CTRL-O Open { if (iteration == 0) { Assert.Equal ("", msg); - sb.NewKeyDownEvent (Key.Q.WithCtrl); + Application.OnKeyDown (Application.QuitKey); } else if (iteration == 1) { Assert.Equal ("Quiting...", msg); msg = ""; - sb.NewMouseEvent (new() { Position = new (1, 24), Flags = MouseFlags.Button1Clicked }); + sb.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); } else { @@ -183,25 +143,64 @@ CTRL-O Open { { var sb = new StatusBar (); - Assert.Empty (sb.Items); - Assert.False (sb.CanFocus); + Assert.Empty (sb.Subviews); + Assert.True (sb.CanFocus); Assert.Equal (Colors.ColorSchemes ["Menu"], sb.ColorScheme); Assert.Equal (0, sb.X); Assert.Equal ("AnchorEnd()", sb.Y.ToString ()); Assert.Equal (Dim.Fill (), sb.Width); - Assert.Equal (1, sb.Height); + Assert.Equal (1, sb.Frame.Height); } [Fact] - public void StatusItem_Constructor () + public void RemoveAndThenAddStatusBar_ShouldNotChangeWidth () { - Application.Init (); - var si = new StatusItem (Application.QuitKey, $"{Application.QuitKey} to Quit", null); - Assert.Equal (KeyCode.CtrlMask | KeyCode.Q, si.Shortcut); - Assert.Equal ($"{Application.QuitKey} to Quit", si.Title); - Assert.Null (si.Action); - si = new (Application.QuitKey, $"{Application.QuitKey} to Quit", () => { }); - Assert.NotNull (si.Action); - Application.Shutdown (); + StatusBar statusBar; + StatusBar statusBar2; + + var w = new Window (); + statusBar2 = new StatusBar () { Id = "statusBar2" }; + statusBar = new StatusBar () { Id = "statusBar" }; + w.Width = Dim.Fill (0); + w.Height = Dim.Fill (0); + w.X = 0; + w.Y = 0; + + w.Visible = true; + w.Modal = false; + w.Title = ""; + statusBar.Width = Dim.Fill (0); + statusBar.Height = 1; + statusBar.X = 0; + statusBar.Y = 0; + statusBar.Visible = true; + w.Add (statusBar); + Assert.Equal (w.StatusBar, statusBar); + + statusBar2.Width = Dim.Fill (0); + statusBar2.Height = 1; + statusBar2.X = 0; + statusBar2.Y = 4; + statusBar2.Visible = true; + w.Add (statusBar2); + Assert.Equal (w.StatusBar, statusBar2); + + var menuBars = w.Subviews.OfType ().ToArray (); + Assert.Equal (2, menuBars.Length); + + Assert.Equal (Dim.Fill (0), menuBars [0].Width); + Assert.Equal (Dim.Fill (0), menuBars [1].Width); + + // Goes wrong here + w.Remove (statusBar); + w.Remove (statusBar2); + + w.Add (statusBar); + w.Add (statusBar2); + + // These assertions fail + Assert.Equal (Dim.Fill (0), menuBars [0].Width); + Assert.Equal (Dim.Fill (0), menuBars [1].Width); } + } diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index cce1f781f..f1a378cbf 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -627,42 +627,40 @@ public class ToplevelTests (ITestOutputHelper output) Assert.Equal (win1, Application.Current); Assert.Equal (win1, Application.OverlappedChildren [0]); win1.Running = true; - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Application.QuitKey)); + Assert.True (Application.OnKeyDown (Application.QuitKey)); Assert.False (isRunning); Assert.False (win1.Running); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.True ( - Application.OverlappedChildren [0].NewKeyDownEvent (Key.Z.WithCtrl) + Application.OnKeyDown (Key.Z.WithCtrl) ); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.F5)); // refresh + Assert.True (Application.OnKeyDown (Key.F5)); // refresh - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.True (win1.IsCurrentTop); Assert.Equal (tvW1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithShift) + Application.OnKeyDown (Key.Tab.WithShift) ); Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); Assert.True ( - Application.OverlappedChildren [0] - .NewKeyDownEvent (Key.Tab.WithCtrl) + Application.OnKeyDown (Key.Tab.WithCtrl) ); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorRight)); + Assert.True (Application.OnKeyDown (Key.CursorRight)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); #if UNIX_KEY_BINDINGS @@ -676,13 +674,13 @@ public class ToplevelTests (ITestOutputHelper output) ); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.OnKeyDown (Key.CursorLeft)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorUp)); + Assert.True (Application.OnKeyDown (Key.CursorUp)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.Tab)); + Assert.True (Application.OnKeyDown (Key.Tab)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); @@ -701,23 +699,23 @@ public class ToplevelTests (ITestOutputHelper output) ); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Application.AlternateForwardKey)); + Assert.True (Application.OnKeyDown (Application.AlternateForwardKey)); Assert.Equal (win2, Application.OverlappedChildren [0]); Assert.Equal (tf2W2, win2.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Application.AlternateBackwardKey)); + Assert.True (Application.OnKeyDown (Application.AlternateBackwardKey)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.B.WithCtrl))); #else - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorLeft)); + Assert.True (Application.OnKeyDown (Key.CursorLeft)); #endif Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf1W1, win1.MostFocused); - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tvW1, win1.MostFocused); Assert.Equal (Point.Empty, tvW1.CursorPosition); @@ -732,7 +730,7 @@ public class ToplevelTests (ITestOutputHelper output) #if UNIX_KEY_BINDINGS Assert.True (Application.OverlappedChildren [0].ProcessKeyDown (new (Key.F.WithCtrl))); #else - Assert.True (Application.OverlappedChildren [0].NewKeyDownEvent (Key.CursorRight)); + Assert.True (Application.OnKeyDown (Key.CursorRight)); #endif Assert.Equal (win1, Application.OverlappedChildren [0]); Assert.Equal (tf2W1, win1.MostFocused); diff --git a/UnitTests/Views/WindowTests.cs b/UnitTests/Views/WindowTests.cs index 3c1fbf13a..24ea77876 100644 --- a/UnitTests/Views/WindowTests.cs +++ b/UnitTests/Views/WindowTests.cs @@ -45,14 +45,7 @@ public class WindowTests ] }; - var sb = new StatusBar ( - new StatusItem [] - { - new ((KeyCode)Key.Q.WithCtrl, "~^Q~ Quit", null), - new ((KeyCode)Key.O.WithCtrl, "~^O~ Open", null), - new ((KeyCode)Key.C.WithCtrl, "~^C~ Copy", null) - } - ); + var sb = new StatusBar (); var fv = new FrameView { Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), Title = "Frame View" }; var win = new Window (); @@ -72,7 +65,7 @@ public class WindowTests ││ ││ ││ ││ │└────────────────┘│ -│ ^Q Quit │ ^O Open│ +│ │ └──────────────────┘", _output ); @@ -99,7 +92,7 @@ public class WindowTests ││ ││ ││ ││ │└────────────────────────────────────┘│ -│ ^Q Quit │ ^O Open │ ^C Copy │ +│ │ └──────────────────────────────────────┘", _output ); @@ -116,7 +109,7 @@ public class WindowTests ││ ││ ││ ││ │└────────────────┘│ -│ ^Q Quit │ ^O Open│ +│ │ └──────────────────┘", _output ); @@ -168,9 +161,9 @@ public class WindowTests Assert.Equal (0, windowWithFrameRectEmpty.Width); Assert.Equal (0, windowWithFrameRectEmpty.Height); Assert.False (windowWithFrameRectEmpty.IsCurrentTop); - #if DEBUG +#if DEBUG Assert.Equal (windowWithFrameRectEmpty.Title, windowWithFrameRectEmpty.Id); - #endif +#endif Assert.False (windowWithFrameRectEmpty.WantContinuousButtonPressed); Assert.False (windowWithFrameRectEmpty.WantMousePositionReports); Assert.Null (windowWithFrameRectEmpty.SuperView); @@ -183,11 +176,11 @@ public class WindowTests windowWithFrame1234.Title = "title"; Assert.Equal ("title", windowWithFrame1234.Title); Assert.NotNull (windowWithFrame1234); - #if DEBUG +#if DEBUG Assert.Equal ($"Window(title){windowWithFrame1234.Frame}", windowWithFrame1234.ToString ()); - #else +#else Assert.Equal ($"Window(){windowWithFrame1234.Frame}", windowWithFrame1234.ToString ()); - #endif +#endif Assert.True (windowWithFrame1234.CanFocus); Assert.False (windowWithFrame1234.HasFocus); Assert.Equal (new (0, 0, 1, 2), windowWithFrame1234.Viewport); @@ -199,9 +192,9 @@ public class WindowTests Assert.Equal (3, windowWithFrame1234.Width); Assert.Equal (4, windowWithFrame1234.Height); Assert.False (windowWithFrame1234.IsCurrentTop); - #if DEBUG +#if DEBUG Assert.Equal (windowWithFrame1234.Title, windowWithFrame1234.Id); - #endif +#endif Assert.False (windowWithFrame1234.WantContinuousButtonPressed); Assert.False (windowWithFrame1234.WantMousePositionReports); Assert.Null (windowWithFrame1234.SuperView); diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index edc258ab4..d5eba8b3e 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -43,17 +43,14 @@ A **HotKey** is a keypress that selects a visible UI item. For selecting items a By default, the `Text` of a `View` is used to determine the `HotKey` by looking for the first occurrence of the [HotKeySpecifier](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_HotKeySpecifier) (which is underscore (`_`) by default). The character following the underscore is the `HotKey`. If the `HotKeySpecifier` is not found in `Text`, the first character of `Text` is used as the `HotKey`. The `Text` of a `View` can be changed at runtime, and the `HotKey` will be updated accordingly. [HotKey](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_HotKey) is `virtual` enabling this behavior to be customized. -### **[Shortcut](~/api/Terminal.Gui.MenuItem.yml#Terminal_Gui_MenuItem_Shortcut)** +### **[Shortcut](~/api/Terminal.Gui.Shortcut.yml) - An opinionated (visually & API) View for displaying a command, helptext, key. +** -A **Shortcut** is a keypress that invokes a [Command](~/api/Terminal.Gui.Command.yml) or `View`-defined action regardless of whether the `View` that defines them is visible (but the `View` must be enabled). Shortcuts can be any keypress; `Key.A`, `Key.A | Key.Ctrl`, `Key.A | Key.Ctrl | Key.Alt`, `Key.Del`, and `Key.F1`, are all valid. +A **Shortcut** is a keypress that invokes a [Command](~/api/Terminal.Gui.Command.yml) or `View`-defined action even if the `View` that defines them is not focused or visible (but the `View` must be enabled). Shortcuts can be any keypress; `Key.A`, `Key.A | Key.Ctrl`, `Key.A | Key.Ctrl | Key.Alt`, `Key.Del`, and `Key.F1`, are all valid. `Shortcuts` are used to define application-wide actions (e.g. `Quit`), or actions that are not visible (e.g. `Copy`). -Not all `Views` support `Shortcut`s. [MenuBar](~/api/Terminal.Gui.MenuBar.yml), [ContextMenu](~/api/Terminal.Gui.ContextMenu.yml), and [StatusBar](~/api/Terminal.Gui.StatusBar.yml) support `Shortcut`s. However, the `Button` class does not. - -The `Shortcut` is provided by setting the [Shortcut](~/api/Terminal.Gui.MenuItem.yml#Terminal_Gui_MenuItem_Shortcut) property on either a [MenuItem](~/api/Terminal.Gui.MenuItem.yml) or [StatusItem](~/api/Terminal.Gui.StatusItem.yml). - -The [ShortcutDelimiter](~/api/Terminal.Gui.MenuBar.yml#Terminal_Gui_MenuBar_ShortcutDelimiter) (`+` by default) is used to separate the `Shortcut` from the `Text` of the `MenuItem` or `StatusItem`. For example, the `Shortcut` for `Quit` is `Ctrl+Q` and the `Text` is `Quit`. +[MenuBar](~/api/Terminal.Gui.MenuBar.yml), [ContextMenu](~/api/Terminal.Gui.ContextMenu.yml), and [StatusBar](~/api/Terminal.Gui.StatusBar.yml) support `Shortcut`s. ### **[Handling Keyboard Events](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_KeyDown)** diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index 258e7c2a6..4d6c09267 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -164,6 +164,7 @@ In v1, scrolling was enabled by using `ScrollView` or `ScrollBarView`. In v2, th The API for handling keyboard input is significantly improved. See [Keyboard API](keyboard.md). * The [Key](~/api/Terminal.Gui.Key.yml) class replaces the `KeyEvent` struct and provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level [KeyCode](~/api/Terminal.Gui.KeyCode.yml) enum when possible. See [Key](~/api/Terminal.Gui.Key.yml) for more details. +* The preferred way to enable Application-wide or View-heirarchy-dependent keystrokes is to use the [Shortcut](~/api/Terminal.Gui.Shortcut.yml) View or the built-in View's that utilize it, such as the [Bar](~/api/Terminal.Gui.Bar.yml)-based views. * The preferred way to handle single keystrokes is to use **Key Bindings**. Key Bindings map a key press to a [Command](~/api/Terminal.Gui.Command.yml). A view can declare which commands it supports, and provide a lambda that implements the functionality of the command, using `View.AddCommand()`. Use the [View.Keybindings](~/api/Terminal.Gui.View.Keybindings.yml) to configure the key bindings. ### How to Fix @@ -201,7 +202,6 @@ The cursor and focus system has been redesigned in v2 to be more consistent and * Set [View.CursorVisibility](~/api/Terminal.Gui.View.CursorVisibility.yml) to the cursor style you want to use. * Remove any overrides of `OnEnter` and `OnLeave` that explicitly change the cursor. - ## Events now use `object sender, EventArgs args` signature Previously events in Terminal.Gui used a mixture of `Action` (no arguments), `Action` (or other raw datatype) and `Action`. Now all events use the `EventHandler` [standard .net design pattern](https://learn.microsoft.com/en-us/dotnet/csharp/event-pattern#event-delegate-signatures). @@ -274,3 +274,23 @@ The [Aligner](~/api/Terminal.Gui.Aligner.yml) class makes it easy to align eleme ### How to Fix * Replace `VerticalAlignment.Middle` is now [Alignment.Center](~/api/Terminal.Gui.Alignment.Center.yml). + +## `StatusBar`- `StatusItem` is replaced by `Shortcut` + +[StatusBar](~/api/Terminal.Gui.StatusBar.yml) has been upgraded to utilize [Shortcut](~/api/Terminal.Gui.Shortcut.yml). + +### How to Fix + +```diff +- var statusBar = new StatusBar ( +- new StatusItem [] +- { +- new ( +- Application.QuitKey, +- $"{Application.QuitKey} to Quit", +- () => Quit () +- ) +- } +- ); ++ var statusBar = new StatusBar (new Shortcut [] { new (Application.QuitKey, "Quit", Quit) }); +``` diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index 9ea1d0749..3d234aad2 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -12,7 +12,7 @@ Apps built with Terminal.Gui now feel modern thanks to these improvements: * *Enhanced Borders and Padding* - Terminal.Gui now supports a `Border`, `Margin`, and `Padding` property on all views. This simplifies View development and enables a sophisticated look and feel. See [Adornments](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#adornments) for details. * *User Configurable Color Themes* - See [Color Themes](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#color-themes) for details. * *Enhanced Unicode/Wide Character support* - Terminal.Gui now supports the full range of Unicode/wide characters. See [Unicode](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#unicode) for details. -* *Line Canvas* - Terminal.Gui now supports a line canvas enabling high-performance drawing of lines and shapes using box-drawing glyphs. `LineCanvas` provides *auto join*, a smart TUI drawing system that automatically selects the correct line/box drawing glyphs for intersections making drawing complex shapes easy. See [Line Canvas](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#line-canvas) for details. +* *[LineCanvas](~/api/Terminal.Gui.Line Canvas.yml)* - Terminal.Gui now supports a line canvas enabling high-performance drawing of lines and shapes using box-drawing glyphs. `LineCanvas` provides *auto join*, a smart TUI drawing system that automatically selects the correct line/box drawing glyphs for intersections making drawing complex shapes easy. See [Line Canvas](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#line-canvas) for details. ## Simplified API @@ -34,15 +34,16 @@ The entire library has been reviewed and simplified. As a result, the API is mor ## New and Improved Built-in Views -* *DatePicker* - NEW! -* *ScrollView* - Replaced by built-in scrolling. -* *ScrollBar* - Replaces *ScrollBarView* with a much simpler view. -* *Slider* - NEW! -* *Bars* - NEW! -* *StatusBar* - New implementation based on `Bar` -* *MenuBar* - New implementation based on `Bar` -* *ContextMenu* - New implementation based on `Bar` -* *File Dialog* - The new, modern file dialog includes icons (in TUI!) for files/folders, search, and a `TreeView``. See [FileDialog](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#filedialog) for details. +* *[DatePicker](~/api/Terminal.Gui.DatePicker.yml)* - NEW! +* *[ScrollView](~/api/Terminal.Gui.ScrollView.yml)* - Replaced by built-in scrolling. +* *[ScrollBar](~/api/Terminal.Gui.ScrollBar.yml)* - Replaces *ScrollBarView* with a much simpler view. +* *[Slider](~/api/Terminal.Gui.Slider.yml)* - NEW! +* *[Shortcut](~/api/Terminal.Gui.Shortcut.yml)* - NEW! An opinionated (visually & API) View for displaying a command, helptext, key. +* *[Bar](~/api/Terminal.Gui.Bar.yml)* - NEW! Building-block View for containing Shortcuts. Opinionated relative to Orientation but minimially so. The basis for the new StatusBar, MenuBar, and Menu views. +* *[StatusBar](~/api/Terminal.Gui.StatusBar.yml)* - New implementation based on `Bar` +* *[MenuBar](~/api/Terminal.Gui.MenuBar.yml)* - COMING SOON! New implementation based on `Bar` +* *[ContextMenu](~/api/Terminal.Gui.ContextMenu.yml)* - COMING SOON! New implementation based on `Bar` +* *[FileDialog](~/api/Terminal.Gui.FileDialog.yml)* - The new, modern file dialog includes icons (in TUI!) for files/folders, search, and a `TreeView`. ## Configuration Manager